implement nested tool calls and initial setup for result metadata

This commit is contained in:
Kujtim Hoxha
2025-04-12 14:49:01 +02:00
parent 8d874b839d
commit 0697dcc1d9
14 changed files with 585 additions and 168 deletions

View File

@@ -77,21 +77,20 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case AgentWorkingMsg:
m.agentWorking = bool(msg)
case tea.KeyMsg:
if key.Matches(msg, focusedKeyMaps.Send) {
// if the key does not match any binding, return
if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Send) {
return m, m.send()
}
if key.Matches(msg, bluredKeyMaps.Send) {
if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Send) {
return m, m.send()
}
if key.Matches(msg, focusedKeyMaps.Blur) {
if m.textarea.Focused() && 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)))
}
if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Focus) {
m.textarea.Focus()
return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
}
}
m.textarea, cmd = m.textarea.Update(msg)

View File

@@ -1,16 +1,21 @@
package chat
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"math"
"strings"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/kujtimiihoxha/termai/internal/app"
"github.com/kujtimiihoxha/termai/internal/llm/agent"
"github.com/kujtimiihoxha/termai/internal/llm/models"
"github.com/kujtimiihoxha/termai/internal/llm/tools"
"github.com/kujtimiihoxha/termai/internal/message"
"github.com/kujtimiihoxha/termai/internal/pubsub"
"github.com/kujtimiihoxha/termai/internal/session"
@@ -18,10 +23,20 @@ import (
"github.com/kujtimiihoxha/termai/internal/tui/util"
)
type uiMessageType int
const (
userMessageType uiMessageType = iota
assistantMessageType
toolMessageType
)
type uiMessage struct {
position int
height int
content string
ID string
messageType uiMessageType
position int
height int
content string
}
type messagesCmp struct {
@@ -32,141 +47,116 @@ type messagesCmp struct {
session session.Session
messages []message.Message
uiMessages []uiMessage
currentIndex int
currentMsgID string
renderer *glamour.TermRenderer
focusRenderer *glamour.TermRenderer
cachedContent map[string]string
agentWorking bool
spinner spinner.Model
needsRerender bool
lastViewport 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"
})
return tea.Batch(m.viewport.Init())
}
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case AgentWorkingMsg:
m.agentWorking = bool(msg)
if m.agentWorking {
cmds = append(cmds, m.spinner.Tick)
}
case EditorFocusMsg:
m.writingMode = bool(msg)
case SessionSelectedMsg:
if msg.ID != m.session.ID {
cmd := m.SetSession(msg)
m.needsRerender = true
return m, cmd
}
return m, nil
case SessionClearedMsg:
m.session = session.Session{}
m.messages = make([]message.Message, 0)
m.currentMsgID = ""
m.needsRerender = true
return m, nil
case tea.KeyMsg:
if m.writingMode {
return m, nil
}
case pubsub.Event[message.Message]:
if msg.Type == pubsub.CreatedEvent {
if msg.Payload.SessionID == m.session.ID {
// check if message exists
messageExists := false
for _, v := range m.messages {
if v.ID == msg.Payload.ID {
return m, nil
messageExists = true
break
}
}
m.messages = append(m.messages, msg.Payload)
m.renderView()
m.viewport.GotoBottom()
if !messageExists {
m.messages = append(m.messages, msg.Payload)
delete(m.cachedContent, m.currentMsgID)
m.currentMsgID = msg.Payload.ID
m.needsRerender = true
}
}
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()
m.needsRerender = true
}
}
}
} else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
for i, v := range m.messages {
if v.ID == msg.Payload.ID {
if !m.messages[i].IsFinished() && msg.Payload.IsFinished() && msg.Payload.FinishReason() == "end_turn" || msg.Payload.FinishReason() == "canceled" {
cmds = append(cmds, util.CmdHandler(AgentWorkingMsg(false)))
}
m.messages[i] = msg.Payload
delete(m.cachedContent, msg.Payload.ID)
m.renderView()
if i == len(m.messages)-1 {
m.viewport.GotoBottom()
}
m.needsRerender = true
break
}
}
}
}
if m.agentWorking {
u, cmd := m.spinner.Update(msg)
m.spinner = u
cmds = append(cmds, cmd)
}
oldPos := m.viewport.YPosition
u, cmd := m.viewport.Update(msg)
m.viewport = u
return m, cmd
m.needsRerender = m.needsRerender || m.viewport.YPosition != oldPos
cmds = append(cmds, cmd)
if m.needsRerender {
m.renderView()
if len(m.messages) > 0 {
if msg, ok := msg.(pubsub.Event[message.Message]); ok {
if (msg.Type == pubsub.CreatedEvent) ||
(msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
m.viewport.GotoBottom()
}
}
}
m.needsRerender = false
}
return m, tea.Batch(cmds...)
}
func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string {
func (m *messagesCmp) renderSimpleMessage(msg message.Message, info ...string) string {
if v, ok := m.cachedContent[msg.ID]; ok {
return v
}
@@ -178,7 +168,7 @@ func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string {
BorderStyle(lipgloss.ThickBorder())
renderer := m.renderer
if inx == m.currentIndex {
if msg.ID == m.currentMsgID {
style = style.
Foreground(styles.Forground).
BorderForeground(styles.Blue).
@@ -186,33 +176,269 @@ func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string {
renderer = m.focusRenderer
}
c, _ := renderer.Render(msg.Content().String())
col, _ := hexToBgSGR(styles.Background.Dark)
rendered := style.Render(forceReplaceBackgroundColors(c, col))
parts := []string{
styles.ForceReplaceBackgroundWithLipgloss(c, styles.Background),
}
// remove newline at the end
parts[0] = strings.TrimSuffix(parts[0], "\n")
if len(info) > 0 {
parts = append(parts, info...)
}
rendered := style.Render(
lipgloss.JoinVertical(
lipgloss.Left,
parts...,
),
)
m.cachedContent[msg.ID] = rendered
return rendered
}
func formatTimeDifference(unixTime1, unixTime2 int64) string {
diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
if diffSeconds < 60 {
return fmt.Sprintf("%.1fs", diffSeconds)
}
minutes := int(diffSeconds / 60)
seconds := int(diffSeconds) % 60
return fmt.Sprintf("%dm%ds", minutes, seconds)
}
func (m *messagesCmp) renderToolCall(toolCall message.ToolCall, isNested bool) string {
key := ""
value := ""
switch toolCall.Name {
// TODO: add result data to the tools
case agent.AgentToolName:
key = "Task"
var params agent.AgentParams
json.Unmarshal([]byte(toolCall.Input), &params)
value = params.Prompt
// TODO: handle nested calls
case tools.BashToolName:
key = "Bash"
var params tools.BashParams
json.Unmarshal([]byte(toolCall.Input), &params)
value = params.Command
case tools.EditToolName:
key = "Edit"
var params tools.EditParams
json.Unmarshal([]byte(toolCall.Input), &params)
value = params.FilePath
case tools.FetchToolName:
key = "Fetch"
var params tools.FetchParams
json.Unmarshal([]byte(toolCall.Input), &params)
value = params.URL
case tools.GlobToolName:
key = "Glob"
var params tools.GlobParams
json.Unmarshal([]byte(toolCall.Input), &params)
if params.Path == "" {
params.Path = "."
}
value = fmt.Sprintf("%s (%s)", params.Pattern, params.Path)
case tools.GrepToolName:
key = "Grep"
var params tools.GrepParams
json.Unmarshal([]byte(toolCall.Input), &params)
if params.Path == "" {
params.Path = "."
}
value = fmt.Sprintf("%s (%s)", params.Pattern, params.Path)
case tools.LSToolName:
key = "Ls"
var params tools.LSParams
json.Unmarshal([]byte(toolCall.Input), &params)
if params.Path == "" {
params.Path = "."
}
value = params.Path
case tools.SourcegraphToolName:
key = "Sourcegraph"
var params tools.SourcegraphParams
json.Unmarshal([]byte(toolCall.Input), &params)
value = params.Query
case tools.ViewToolName:
key = "View"
var params tools.ViewParams
json.Unmarshal([]byte(toolCall.Input), &params)
value = params.FilePath
case tools.WriteToolName:
key = "Write"
var params tools.WriteParams
json.Unmarshal([]byte(toolCall.Input), &params)
value = params.FilePath
default:
key = toolCall.Name
var params map[string]any
json.Unmarshal([]byte(toolCall.Input), &params)
jsonData, _ := json.Marshal(params)
value = string(jsonData)
}
style := styles.BaseStyle.
Width(m.width).
BorderLeft(true).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1).
BorderForeground(styles.Yellow)
keyStyle := styles.BaseStyle.
Foreground(styles.ForgroundDim)
valyeStyle := styles.BaseStyle.
Foreground(styles.Forground)
if isNested {
valyeStyle = valyeStyle.Foreground(styles.ForgroundMid)
}
keyValye := keyStyle.Render(
fmt.Sprintf("%s: ", key),
)
if !isNested {
value = valyeStyle.
Width(m.width - lipgloss.Width(keyValye) - 2).
Render(
ansi.Truncate(
value,
m.width-lipgloss.Width(keyValye)-2,
"...",
),
)
} else {
keyValye = keyStyle.Render(
fmt.Sprintf(" └ %s: ", key),
)
value = valyeStyle.
Width(m.width - lipgloss.Width(keyValye) - 2).
Render(
ansi.Truncate(
value,
m.width-lipgloss.Width(keyValye)-2,
"...",
),
)
}
innerToolCalls := make([]string, 0)
if toolCall.Name == agent.AgentToolName {
messages, _ := m.app.Messages.List(toolCall.ID)
toolCalls := make([]message.ToolCall, 0)
for _, v := range messages {
toolCalls = append(toolCalls, v.ToolCalls()...)
}
for _, v := range toolCalls {
call := m.renderToolCall(v, true)
innerToolCalls = append(innerToolCalls, call)
}
}
if isNested {
return lipgloss.JoinHorizontal(
lipgloss.Left,
keyValye,
value,
)
}
callContent := lipgloss.JoinHorizontal(
lipgloss.Left,
keyValye,
value,
)
callContent = strings.ReplaceAll(callContent, "\n", "")
if len(innerToolCalls) > 0 {
callContent = lipgloss.JoinVertical(
lipgloss.Left,
callContent,
lipgloss.JoinVertical(
lipgloss.Left,
innerToolCalls...,
),
)
}
return style.Render(callContent)
}
func (m *messagesCmp) renderAssistantMessage(msg message.Message) []uiMessage {
// find the user message that is before this assistant message
var userMsg message.Message
for i := len(m.messages) - 1; i >= 0; i-- {
if m.messages[i].Role == message.User {
userMsg = m.messages[i]
break
}
}
messages := make([]uiMessage, 0)
if msg.Content().String() != "" {
info := make([]string, 0)
if msg.IsFinished() && msg.FinishReason() == "end_turn" {
finish := msg.FinishPart()
took := formatTimeDifference(userMsg.CreatedAt, finish.Time)
info = append(info, styles.BaseStyle.Width(m.width-1).Foreground(styles.ForgroundDim).Render(
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
))
}
content := m.renderSimpleMessage(msg, info...)
messages = append(messages, uiMessage{
messageType: assistantMessageType,
position: 0, // gets updated in renderView
height: lipgloss.Height(content),
content: content,
})
}
for _, v := range msg.ToolCalls() {
content := m.renderToolCall(v, false)
messages = append(messages,
uiMessage{
messageType: toolMessageType,
position: 0, // gets updated in renderView
height: lipgloss.Height(content),
content: content,
},
)
}
return messages
}
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)
content := m.renderSimpleMessage(v)
m.uiMessages = append(m.uiMessages, uiMessage{
messageType: userMessageType,
position: pos,
height: lipgloss.Height(content),
content: content,
})
pos += lipgloss.Height(content) + 1 // + 1 for spacing
case message.Assistant:
assistantMessages := m.renderAssistantMessage(v)
for _, msg := range assistantMessages {
msg.position = pos
m.uiMessages = append(m.uiMessages, msg)
pos += msg.height + 1 // + 1 for spacing
}
}
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)
messages = append(messages, v.content,
styles.BaseStyle.
Width(m.width).
Render(
"",
),
)
}
m.viewport.SetContent(
styles.BaseStyle.
@@ -246,7 +472,6 @@ func (m *messagesCmp) View() string {
)
}
m.renderView()
return styles.BaseStyle.
Width(m.width).
Render(
@@ -260,15 +485,21 @@ func (m *messagesCmp) View() string {
func (m *messagesCmp) help() string {
text := ""
if m.agentWorking {
text += styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render(
fmt.Sprintf("%s %s ", m.spinner.View(), "Generating..."),
)
}
if m.writingMode {
text = lipgloss.JoinHorizontal(
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(
text += lipgloss.JoinHorizontal(
lipgloss.Left,
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
@@ -306,7 +537,15 @@ func (m *messagesCmp) SetSize(width, height int) {
glamour.WithWordWrap(width-1),
)
m.focusRenderer = focusRenderer
// clear the cached content
for k := range m.cachedContent {
delete(m.cachedContent, k)
}
m.renderer = renderer
if len(m.messages) > 0 {
m.renderView()
m.viewport.GotoBottom()
}
}
func (m *messagesCmp) GetSize() (int, int) {
@@ -320,7 +559,8 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
return util.ReportError(err)
}
m.messages = messages
m.messages = append(m.messages, m.messages[0])
m.currentMsgID = m.messages[len(m.messages)-1].ID
m.needsRerender = true
return nil
}
@@ -333,6 +573,9 @@ func NewMessagesCmp(app *app.App) tea.Model {
glamour.WithStyles(styles.MarkdownTheme(false)),
glamour.WithWordWrap(80),
)
s := spinner.New()
s.Spinner = spinner.Pulse
return &messagesCmp{
app: app,
writingMode: true,
@@ -340,5 +583,6 @@ func NewMessagesCmp(app *app.App) tea.Model {
viewport: viewport.New(0, 0),
focusRenderer: focusRenderer,
renderer: renderer,
spinner: s,
}
}

View File

@@ -5,6 +5,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/kujtimiihoxha/termai/internal/pubsub"
"github.com/kujtimiihoxha/termai/internal/session"
"github.com/kujtimiihoxha/termai/internal/tui/styles"
)
@@ -19,6 +20,14 @@ func (m *sidebarCmp) Init() tea.Cmd {
}
func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case pubsub.Event[session.Session]:
if msg.Type == pubsub.UpdatedEvent {
if m.session.ID == msg.Payload.ID {
m.session = msg.Payload
}
}
}
return m, nil
}
@@ -45,7 +54,7 @@ func (m *sidebarCmp) sessionSection() string {
sessionValue := styles.BaseStyle.
Foreground(styles.Forground).
Width(m.width - lipgloss.Width(sessionKey)).
Render(": New Session")
Render(fmt.Sprintf(": %s", m.session.Title))
return lipgloss.JoinHorizontal(
lipgloss.Left,
sessionKey,

View File

@@ -1,9 +1,10 @@
package page
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/kujtimiihoxha/termai/internal/app"
"github.com/kujtimiihoxha/termai/internal/message"
"github.com/kujtimiihoxha/termai/internal/llm/agent"
"github.com/kujtimiihoxha/termai/internal/session"
"github.com/kujtimiihoxha/termai/internal/tui/components/chat"
"github.com/kujtimiihoxha/termai/internal/tui/layout"
@@ -18,8 +19,32 @@ type chatPage struct {
session session.Session
}
type ChatKeyMap struct {
NewSession key.Binding
}
var keyMap = ChatKeyMap{
NewSession: key.NewBinding(
key.WithKeys("ctrl+n"),
key.WithHelp("ctrl+n", "new session"),
),
}
func (p *chatPage) Init() tea.Cmd {
return p.layout.Init()
// TODO: remove
cmds := []tea.Cmd{
p.layout.Init(),
}
sessions, _ := p.app.Sessions.List()
if len(sessions) > 0 {
p.session = sessions[0]
cmd := p.setSidebar()
cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(p.session)), cmd)
}
return tea.Batch(
cmds...,
)
}
func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -31,6 +56,13 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if cmd != nil {
return p, cmd
}
case tea.KeyMsg:
switch {
case key.Matches(msg, keyMap.NewSession):
p.session = session.Session{}
p.clearSidebar()
return p, util.CmdHandler(chat.SessionClearedMsg{})
}
}
u, cmd := p.layout.Update(msg)
p.layout = u.(layout.SplitPaneLayout)
@@ -51,6 +83,12 @@ func (p *chatPage) setSidebar() tea.Cmd {
return sidebarContainer.Init()
}
func (p *chatPage) clearSidebar() {
p.layout.SetRightPanel(nil)
width, height := p.layout.GetSize()
p.layout.SetSize(width, height)
}
func (p *chatPage) sendMessage(text string) tea.Cmd {
var cmds []tea.Cmd
if p.session.ID == "" {
@@ -66,15 +104,15 @@ func (p *chatPage) sendMessage(text string) tea.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,
},
},
})
// TODO: move this to a service
a, err := agent.NewCoderAgent(p.app)
if err != nil {
return util.ReportError(err)
}
go func() {
a.Generate(p.app.Context, p.session.ID, text)
}()
return tea.Batch(cmds...)
}
@@ -85,7 +123,7 @@ func (p *chatPage) View() string {
func NewChatPage(app *app.App) tea.Model {
messagesContainer := layout.NewContainer(
chat.NewMessagesCmp(app),
layout.WithPadding(1, 1, 1, 1),
layout.WithPadding(1, 1, 0, 1),
)
editorContainer := layout.NewContainer(

View File

@@ -0,0 +1,81 @@
package styles
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/charmbracelet/lipgloss"
)
var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
func getColorRGB(c lipgloss.TerminalColor) (uint8, uint8, uint8) {
r, g, b, a := c.RGBA()
// Un-premultiply alpha if needed
if a > 0 && a < 0xffff {
r = (r * 0xffff) / a
g = (g * 0xffff) / a
b = (b * 0xffff) / a
}
// Convert from 16-bit to 8-bit color
return uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)
}
func ForceReplaceBackgroundWithLipgloss(input string, newBgColor lipgloss.TerminalColor) string {
r, g, b := getColorRGB(newBgColor)
newBg := fmt.Sprintf("48;2;%d;%d;%d", r, g, b)
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"
})
}

View File

@@ -515,6 +515,7 @@ var ASCIIStyleConfig = ansi.StyleConfig{
Document: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BackgroundColor: stringPtr(Background.Dark),
Color: stringPtr(ForgroundDim.Dark),
},
Indent: uintPtr(1),
IndentToken: stringPtr(BaseStyle.Render(" ")),
@@ -688,7 +689,7 @@ var DraculaStyleConfig = ansi.StyleConfig{
Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockSuffix: "\n",
Color: stringPtr("#bd93f9"),
Color: stringPtr(PrimaryColor.Dark),
Bold: boolPtr(true),
BackgroundColor: stringPtr(Background.Dark),
},
@@ -740,7 +741,7 @@ var DraculaStyleConfig = ansi.StyleConfig{
},
Strong: ansi.StylePrimitive{
Bold: boolPtr(true),
Color: stringPtr("#ffb86c"),
Color: stringPtr(Blue.Dark),
BackgroundColor: stringPtr(Background.Dark),
},
HorizontalRule: ansi.StylePrimitive{
@@ -796,7 +797,7 @@ var DraculaStyleConfig = ansi.StyleConfig{
CodeBlock: ansi.StyleCodeBlock{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr("#ffb86c"),
Color: stringPtr(Blue.Dark),
BackgroundColor: stringPtr(Background.Dark),
},
Margin: uintPtr(defaultMargin),

View File

@@ -34,6 +34,11 @@ var (
Light: "#d3d3d3",
}
ForgroundMid = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#a0a0a0",
}
ForgroundDim = lipgloss.AdaptiveColor{
Dark: "#737373",
Light: "#737373",
@@ -159,6 +164,11 @@ var (
Light: light.Peach().Hex,
}
Yellow = lipgloss.AdaptiveColor{
Dark: dark.Yellow().Hex,
Light: light.Yellow().Hex,
}
Primary = Blue
Secondary = Mauve