add initial message handling
This commit is contained in:
@@ -7,6 +7,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
const createMessage = `-- name: CreateMessage :one
|
const createMessage = `-- name: CreateMessage :one
|
||||||
@@ -15,19 +16,21 @@ INSERT INTO messages (
|
|||||||
session_id,
|
session_id,
|
||||||
role,
|
role,
|
||||||
parts,
|
parts,
|
||||||
|
model,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (
|
) 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 {
|
type CreateMessageParams struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
SessionID string `json:"session_id"`
|
SessionID string `json:"session_id"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Parts string `json:"parts"`
|
Parts string `json:"parts"`
|
||||||
|
Model sql.NullString `json:"model"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
|
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.SessionID,
|
||||||
arg.Role,
|
arg.Role,
|
||||||
arg.Parts,
|
arg.Parts,
|
||||||
|
arg.Model,
|
||||||
)
|
)
|
||||||
var i Message
|
var i Message
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -43,8 +47,10 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (M
|
|||||||
&i.SessionID,
|
&i.SessionID,
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Parts,
|
&i.Parts,
|
||||||
|
&i.Model,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FinishedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -70,7 +76,7 @@ func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getMessage = `-- name: GetMessage :one
|
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
|
FROM messages
|
||||||
WHERE id = ? LIMIT 1
|
WHERE id = ? LIMIT 1
|
||||||
`
|
`
|
||||||
@@ -83,14 +89,16 @@ func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) {
|
|||||||
&i.SessionID,
|
&i.SessionID,
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Parts,
|
&i.Parts,
|
||||||
|
&i.Model,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FinishedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listMessagesBySession = `-- name: ListMessagesBySession :many
|
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
|
FROM messages
|
||||||
WHERE session_id = ?
|
WHERE session_id = ?
|
||||||
ORDER BY created_at ASC
|
ORDER BY created_at ASC
|
||||||
@@ -110,8 +118,10 @@ func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) (
|
|||||||
&i.SessionID,
|
&i.SessionID,
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Parts,
|
&i.Parts,
|
||||||
|
&i.Model,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FinishedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -130,16 +140,18 @@ const updateMessage = `-- name: UpdateMessage :exec
|
|||||||
UPDATE messages
|
UPDATE messages
|
||||||
SET
|
SET
|
||||||
parts = ?,
|
parts = ?,
|
||||||
|
finished_at = ?,
|
||||||
updated_at = strftime('%s', 'now')
|
updated_at = strftime('%s', 'now')
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateMessageParams struct {
|
type UpdateMessageParams struct {
|
||||||
Parts string `json:"parts"`
|
Parts string `json:"parts"`
|
||||||
ID string `json:"id"`
|
FinishedAt sql.NullInt64 `json:"finished_at"`
|
||||||
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ CREATE TABLE IF NOT EXISTS messages (
|
|||||||
session_id TEXT NOT NULL,
|
session_id TEXT NOT NULL,
|
||||||
role TEXT NOT NULL,
|
role TEXT NOT NULL,
|
||||||
parts TEXT NOT NULL default '[]',
|
parts TEXT NOT NULL default '[]',
|
||||||
|
model TEXT,
|
||||||
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||||
updated_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
|
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
SessionID string `json:"session_id"`
|
SessionID string `json:"session_id"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Parts string `json:"parts"`
|
Parts string `json:"parts"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Model sql.NullString `json:"model"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
FinishedAt sql.NullInt64 `json:"finished_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ INSERT INTO messages (
|
|||||||
session_id,
|
session_id,
|
||||||
role,
|
role,
|
||||||
parts,
|
parts,
|
||||||
|
model,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
@@ -26,9 +27,11 @@ RETURNING *;
|
|||||||
UPDATE messages
|
UPDATE messages
|
||||||
SET
|
SET
|
||||||
parts = ?,
|
parts = ?,
|
||||||
|
finished_at = ?,
|
||||||
updated_at = strftime('%s', 'now')
|
updated_at = strftime('%s', 'now')
|
||||||
WHERE id = ?;
|
WHERE id = ?;
|
||||||
|
|
||||||
|
|
||||||
-- name: DeleteMessage :exec
|
-- name: DeleteMessage :exec
|
||||||
DELETE FROM messages
|
DELETE FROM messages
|
||||||
WHERE id = ?;
|
WHERE id = ?;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package message
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MessageRole string
|
type MessageRole string
|
||||||
@@ -64,6 +65,7 @@ type ToolCall struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Input string `json:"input"`
|
Input string `json:"input"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
Metadata any `json:"metadata"`
|
||||||
Finished bool `json:"finished"`
|
Finished bool `json:"finished"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ func (ToolResult) isPart() {}
|
|||||||
|
|
||||||
type Finish struct {
|
type Finish struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Finish) isPart() {}
|
func (Finish) isPart() {}
|
||||||
@@ -161,6 +164,15 @@ func (m *Message) IsFinished() bool {
|
|||||||
return false
|
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 {
|
func (m *Message) FinishReason() string {
|
||||||
for _, part := range m.Parts {
|
for _, part := range m.Parts {
|
||||||
if c, ok := part.(Finish); ok {
|
if c, ok := part.(Finish); ok {
|
||||||
@@ -232,7 +244,7 @@ func (m *Message) SetToolResults(tr []ToolResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) AddFinish(reason string) {
|
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) {
|
func (m *Message) AddImageURL(url, detail string) {
|
||||||
|
|||||||
@@ -2,17 +2,20 @@ package message
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/kujtimiihoxha/termai/internal/db"
|
"github.com/kujtimiihoxha/termai/internal/db"
|
||||||
|
"github.com/kujtimiihoxha/termai/internal/llm/models"
|
||||||
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateMessageParams struct {
|
type CreateMessageParams struct {
|
||||||
Role MessageRole
|
Role MessageRole
|
||||||
Parts []ContentPart
|
Parts []ContentPart
|
||||||
|
Model models.ModelID
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
@@ -68,6 +71,7 @@ func (s *service) Create(sessionID string, params CreateMessageParams) (Message,
|
|||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
Role: string(params.Role),
|
Role: string(params.Role),
|
||||||
Parts: string(partsJSON),
|
Parts: string(partsJSON),
|
||||||
|
Model: sql.NullString{String: string(params.Model), Valid: true},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Message{}, err
|
return Message{}, err
|
||||||
@@ -101,9 +105,15 @@ func (s *service) Update(message Message) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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{
|
err = s.q.UpdateMessage(s.ctx, db.UpdateMessageParams{
|
||||||
ID: message.ID,
|
ID: message.ID,
|
||||||
Parts: string(parts),
|
Parts: string(parts),
|
||||||
|
FinishedAt: finishedAt,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
113
internal/tui/components/chat/chat.go
Normal file
113
internal/tui/components/chat/chat.go
Normal file
@@ -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/charmbracelet/lipgloss"
|
||||||
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
||||||
"github.com/kujtimiihoxha/termai/internal/tui/styles"
|
"github.com/kujtimiihoxha/termai/internal/tui/styles"
|
||||||
|
"github.com/kujtimiihoxha/termai/internal/tui/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type editorCmp struct {
|
type editorCmp struct {
|
||||||
textarea textarea.Model
|
textarea textarea.Model
|
||||||
|
agentWorking bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type focusedEditorKeyMaps struct {
|
type focusedEditorKeyMaps struct {
|
||||||
@@ -49,39 +51,51 @@ func (m *editorCmp) Init() tea.Cmd {
|
|||||||
return textarea.Blink
|
return textarea.Blink
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *editorCmp) send() tea.Cmd {
|
||||||
var cmd tea.Cmd
|
if m.agentWorking {
|
||||||
if m.textarea.Focused() {
|
return util.ReportWarn("Agent is working, please wait...")
|
||||||
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 tea.KeyMsg:
|
|
||||||
if key.Matches(msg, bluredKeyMaps.Send) {
|
|
||||||
// TODO: send message
|
|
||||||
m.textarea.Reset()
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
if key.Matches(msg, bluredKeyMaps.Focus) {
|
|
||||||
m.textarea.Focus()
|
|
||||||
return m, textarea.Blink
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
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
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
if !m.textarea.Focused() {
|
||||||
|
m.textarea.Focus()
|
||||||
|
return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.textarea, cmd = m.textarea.Update(msg)
|
||||||
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorCmp) View() string {
|
func (m *editorCmp) View() string {
|
||||||
@@ -122,6 +136,7 @@ func NewEditorCmp() tea.Model {
|
|||||||
ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
|
ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
|
||||||
ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
|
ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
|
||||||
ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
|
ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
|
||||||
|
ti.CharLimit = -1
|
||||||
ti.Focus()
|
ti.Focus()
|
||||||
return &editorCmp{
|
return &editorCmp{
|
||||||
textarea: ti,
|
textarea: ti,
|
||||||
|
|||||||
@@ -1,21 +1,344 @@
|
|||||||
package chat
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
func (m *messagesCmp) Init() tea.Cmd {
|
type uiMessage struct {
|
||||||
return nil
|
position int
|
||||||
|
height int
|
||||||
|
content string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
|
type messagesCmp struct {
|
||||||
return m, nil
|
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 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) 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 {
|
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 NewMessagesCmp() tea.Model {
|
func (m *messagesCmp) help() string {
|
||||||
return &messagesCmp{}
|
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(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"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"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/tui/styles"
|
||||||
"github.com/kujtimiihoxha/termai/internal/version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type sidebarCmp struct {
|
type sidebarCmp struct {
|
||||||
width, height int
|
width, height int
|
||||||
|
session session.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *sidebarCmp) Init() tea.Cmd {
|
func (m *sidebarCmp) Init() tea.Cmd {
|
||||||
return nil
|
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
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *sidebarCmp) View() string {
|
func (m *sidebarCmp) View() string {
|
||||||
return styles.BaseStyle.Width(m.width).Render(
|
return styles.BaseStyle.
|
||||||
lipgloss.JoinVertical(
|
Width(m.width).
|
||||||
lipgloss.Top,
|
Height(m.height - 1).
|
||||||
m.header(),
|
Render(
|
||||||
" ",
|
lipgloss.JoinVertical(
|
||||||
m.session(),
|
lipgloss.Top,
|
||||||
" ",
|
header(m.width),
|
||||||
m.modifiedFiles(),
|
" ",
|
||||||
" ",
|
m.sessionSection(),
|
||||||
m.lspsConfigured(),
|
" ",
|
||||||
),
|
m.modifiedFiles(),
|
||||||
)
|
" ",
|
||||||
|
lspsConfigured(m.width),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *sidebarCmp) session() string {
|
func (m *sidebarCmp) sessionSection() string {
|
||||||
sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Render("Session")
|
sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session")
|
||||||
sessionValue := styles.BaseStyle.
|
sessionValue := styles.BaseStyle.
|
||||||
Foreground(styles.Forground).
|
Foreground(styles.Forground).
|
||||||
Width(m.width - lipgloss.Width(sessionKey)).
|
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 {
|
func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
|
||||||
stats := ""
|
stats := ""
|
||||||
if additions > 0 && removals > 0 {
|
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 {
|
} 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 {
|
} 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)
|
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.JoinHorizontal(
|
||||||
lipgloss.Left,
|
lipgloss.Left,
|
||||||
filePathStr,
|
filePathStr,
|
||||||
" ",
|
|
||||||
stats,
|
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 {
|
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 {
|
files := []struct {
|
||||||
path string
|
path string
|
||||||
additions int
|
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) {
|
func (m *sidebarCmp) SetSize(width, height int) {
|
||||||
m.width = width
|
m.width = width
|
||||||
m.height = height
|
m.height = height
|
||||||
@@ -193,6 +114,8 @@ func (m *sidebarCmp) GetSize() (int, int) {
|
|||||||
return m.width, m.height
|
return m.width, m.height
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSidebarCmp() tea.Model {
|
func NewSidebarCmp(session session.Session) tea.Model {
|
||||||
return &sidebarCmp{}
|
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 {
|
type SplitPaneLayout interface {
|
||||||
tea.Model
|
tea.Model
|
||||||
Sizeable
|
Sizeable
|
||||||
|
SetLeftPanel(panel Container)
|
||||||
|
SetRightPanel(panel Container)
|
||||||
|
SetBottomPanel(panel Container)
|
||||||
}
|
}
|
||||||
|
|
||||||
type splitPaneLayout struct {
|
type splitPaneLayout struct {
|
||||||
@@ -160,6 +163,27 @@ func (s *splitPaneLayout) GetSize() (int, int) {
|
|||||||
return s.width, s.height
|
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 {
|
func (s *splitPaneLayout) BindingKeys() []key.Binding {
|
||||||
keys := []key.Binding{}
|
keys := []key.Binding{}
|
||||||
if s.leftPanel != nil {
|
if s.leftPanel != nil {
|
||||||
|
|||||||
@@ -3,28 +3,100 @@ package page
|
|||||||
import (
|
import (
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/kujtimiihoxha/termai/internal/app"
|
"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/components/chat"
|
||||||
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
||||||
|
"github.com/kujtimiihoxha/termai/internal/tui/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChatPage PageID = "chat"
|
var ChatPage PageID = "chat"
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
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 {
|
func NewChatPage(app *app.App) tea.Model {
|
||||||
messagesContainer := layout.NewContainer(
|
messagesContainer := layout.NewContainer(
|
||||||
chat.NewMessagesCmp(),
|
chat.NewMessagesCmp(app),
|
||||||
layout.WithPadding(1, 1, 1, 1),
|
|
||||||
)
|
|
||||||
sidebarContainer := layout.NewContainer(
|
|
||||||
chat.NewSidebarCmp(),
|
|
||||||
layout.WithPadding(1, 1, 1, 1),
|
layout.WithPadding(1, 1, 1, 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
editorContainer := layout.NewContainer(
|
editorContainer := layout.NewContainer(
|
||||||
chat.NewEditorCmp(),
|
chat.NewEditorCmp(),
|
||||||
layout.WithBorder(true, false, false, false),
|
layout.WithBorder(true, false, false, false),
|
||||||
)
|
)
|
||||||
return layout.NewSplitPane(
|
return &chatPage{
|
||||||
layout.WithRightPanel(sidebarContainer),
|
app: app,
|
||||||
layout.WithLeftPanel(messagesContainer),
|
layout: layout.NewSplitPane(
|
||||||
layout.WithBottomPanel(editorContainer),
|
layout.WithLeftPanel(messagesContainer),
|
||||||
)
|
layout.WithBottomPanel(editorContainer),
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,13 @@ var catppuccinDark = ansi.StyleConfig{
|
|||||||
Italic: boolPtr(true),
|
Italic: boolPtr(true),
|
||||||
Prefix: "┃ ",
|
Prefix: "┃ ",
|
||||||
},
|
},
|
||||||
Indent: uintPtr(1),
|
Indent: uintPtr(1),
|
||||||
Margin: uintPtr(defaultMargin),
|
IndentToken: stringPtr(BaseStyle.Render(" ")),
|
||||||
},
|
},
|
||||||
List: ansi.StyleList{
|
List: ansi.StyleList{
|
||||||
LevelIndent: defaultMargin,
|
LevelIndent: defaultMargin,
|
||||||
StyleBlock: ansi.StyleBlock{
|
StyleBlock: ansi.StyleBlock{
|
||||||
|
IndentToken: stringPtr(BaseStyle.Render(" ")),
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
StylePrimitive: ansi.StylePrimitive{
|
||||||
Color: stringPtr(dark.Text().Hex),
|
Color: stringPtr(dark.Text().Hex),
|
||||||
},
|
},
|
||||||
@@ -496,3 +497,444 @@ var catppuccinLight = ansi.StyleConfig{
|
|||||||
Color: stringPtr(light.Sapphire().Hex),
|
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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user