fix(tui): modal visuals
This commit is contained in:
@@ -83,6 +83,6 @@ type HelpDialog interface {
|
|||||||
func NewHelpDialog(commands []commands.Command) HelpDialog {
|
func NewHelpDialog(commands []commands.Command) HelpDialog {
|
||||||
return &helpDialog{
|
return &helpDialog{
|
||||||
commands: commands,
|
commands: commands,
|
||||||
modal: modal.New(),
|
modal: modal.New(modal.WithTitle("Help")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ func (m *modelDialog) switchProvider(offset int) {
|
|||||||
|
|
||||||
m.hScrollOffset = newOffset
|
m.hScrollOffset = newOffset
|
||||||
m.provider = m.availableProviders[m.hScrollOffset]
|
m.provider = m.availableProviders[m.hScrollOffset]
|
||||||
|
m.modal.SetTitle(fmt.Sprintf("Select %s Model", m.provider.Name))
|
||||||
m.setupModelsForProvider(m.provider.Id)
|
m.setupModelsForProvider(m.provider.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,14 +190,6 @@ func (m *modelDialog) View() string {
|
|||||||
Background(t.BackgroundElement()).
|
Background(t.BackgroundElement()).
|
||||||
Foreground(t.Text())
|
Foreground(t.Text())
|
||||||
|
|
||||||
// Capitalize first letter of provider name
|
|
||||||
title := baseStyle.
|
|
||||||
Foreground(t.Primary()).
|
|
||||||
Bold(true).
|
|
||||||
Width(maxDialogWidth).
|
|
||||||
Padding(0, 0, 1).
|
|
||||||
Render(fmt.Sprintf("Select %s Model", m.provider.Name))
|
|
||||||
|
|
||||||
// Render visible models
|
// Render visible models
|
||||||
endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
|
endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
|
||||||
modelItems := make([]string, 0, endIdx-m.scrollOffset)
|
modelItems := make([]string, 0, endIdx-m.scrollOffset)
|
||||||
@@ -217,8 +210,9 @@ func (m *modelDialog) View() string {
|
|||||||
|
|
||||||
content := lipgloss.JoinVertical(
|
content := lipgloss.JoinVertical(
|
||||||
lipgloss.Left,
|
lipgloss.Left,
|
||||||
title,
|
baseStyle.
|
||||||
baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
|
Width(maxDialogWidth).
|
||||||
|
Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
|
||||||
scrollIndicator,
|
scrollIndicator,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -238,12 +232,7 @@ func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.hScrollPossible {
|
if m.hScrollPossible {
|
||||||
if m.hScrollOffset > 0 {
|
indicator = "← " + indicator + "→"
|
||||||
indicator = "← " + indicator
|
|
||||||
}
|
|
||||||
if m.hScrollOffset < len(m.availableProviders)-1 {
|
|
||||||
indicator += "→"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if indicator == "" {
|
if indicator == "" {
|
||||||
@@ -313,6 +302,6 @@ func NewModelDialog(app *app.App) ModelDialog {
|
|||||||
hScrollOffset: 0,
|
hScrollOffset: 0,
|
||||||
hScrollPossible: len(availableProviders) > 1,
|
hScrollPossible: len(availableProviders) > 1,
|
||||||
provider: availableProviders[0],
|
provider: availableProviders[0],
|
||||||
modal: modal.New(),
|
modal: modal.New(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,12 @@ type SessionDialog interface {
|
|||||||
layout.Modal
|
layout.Modal
|
||||||
}
|
}
|
||||||
|
|
||||||
type sessionItem struct {
|
type sessionItem client.SessionInfo
|
||||||
session client.SessionInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s sessionItem) Render(selected bool, width int) string {
|
func (s sessionItem) Render(selected bool, width int) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.BaseStyle().
|
baseStyle := styles.BaseStyle().
|
||||||
Width(width - 2).
|
Width(width - 4).
|
||||||
Background(t.BackgroundElement())
|
Background(t.BackgroundElement())
|
||||||
|
|
||||||
if selected {
|
if selected {
|
||||||
@@ -39,7 +37,7 @@ func (s sessionItem) Render(selected bool, width int) string {
|
|||||||
Foreground(t.Text())
|
Foreground(t.Text())
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseStyle.Padding(0, 1).Render(s.session.Title)
|
return baseStyle.Padding(0, 1).Render(s.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sessionDialog struct {
|
type sessionDialog struct {
|
||||||
@@ -60,15 +58,14 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
s.width = msg.Width
|
s.width = msg.Width
|
||||||
s.height = msg.Height
|
s.height = msg.Height
|
||||||
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||||
case tea.KeyMsg:
|
case tea.KeyPressMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "enter":
|
case "enter":
|
||||||
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
|
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
|
||||||
selectedSession := item.session
|
s.selectedSessionID = item.Id
|
||||||
s.selectedSessionID = selectedSession.Id
|
|
||||||
return s, tea.Sequence(
|
return s, tea.Sequence(
|
||||||
util.CmdHandler(modal.CloseModalMsg{}),
|
util.CmdHandler(modal.CloseModalMsg{}),
|
||||||
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
util.CmdHandler(app.SessionSelectedMsg(&item)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +91,10 @@ func NewSessionDialog(app *app.App) SessionDialog {
|
|||||||
|
|
||||||
var sessionItems []sessionItem
|
var sessionItems []sessionItem
|
||||||
for _, sess := range sessions {
|
for _, sess := range sessions {
|
||||||
sessionItems = append(sessionItems, sessionItem{session: sess})
|
if sess.ParentID != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sessionItems = append(sessionItems, sessionItem(sess))
|
||||||
}
|
}
|
||||||
|
|
||||||
list := list.NewListComponent(
|
list := list.NewListComponent(
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package list
|
package list
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListItem interface {
|
type ListItem interface {
|
||||||
@@ -148,7 +149,7 @@ func (c *listComponent[T]) View() string {
|
|||||||
listItems = append(listItems, title)
|
listItems = append(listItems, title)
|
||||||
}
|
}
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, listItems...)
|
return strings.Join(listItems, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] {
|
func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package modal
|
package modal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
@@ -66,6 +68,10 @@ func New(opts ...ModalOption) *Modal {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Modal) SetTitle(title string) {
|
||||||
|
m.title = title
|
||||||
|
}
|
||||||
|
|
||||||
// Render renders the modal centered on the screen
|
// Render renders the modal centered on the screen
|
||||||
func (m *Modal) Render(contentView string, background string) string {
|
func (m *Modal) Render(contentView string, background string) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
@@ -95,15 +101,21 @@ func (m *Modal) Render(contentView string, background string) string {
|
|||||||
titleStyle := baseStyle.
|
titleStyle := baseStyle.
|
||||||
Foreground(t.Primary()).
|
Foreground(t.Primary()).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Width(innerWidth).
|
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
titleView := titleStyle.Render(m.title)
|
// titleView := titleStyle.Render(m.title)
|
||||||
finalContent = lipgloss.JoinVertical(
|
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
|
||||||
lipgloss.Left,
|
escText := escStyle.Render("esc")
|
||||||
titleView,
|
|
||||||
contentView,
|
// Calculate position for esc text
|
||||||
)
|
titleWidth := lipgloss.Width(m.title)
|
||||||
|
escWidth := lipgloss.Width(escText)
|
||||||
|
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3)
|
||||||
|
spacer := strings.Repeat(" ", spacesNeeded)
|
||||||
|
titleLine := m.title + spacer + escText
|
||||||
|
titleLine = titleStyle.Render(titleLine)
|
||||||
|
|
||||||
|
finalContent = strings.Join([]string{titleLine, contentView}, "\n") + "\n"
|
||||||
} else {
|
} else {
|
||||||
finalContent = contentView
|
finalContent = contentView
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,8 +204,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
a.layout.SetSize(a.width, a.height)
|
a.layout.SetSize(a.width, a.height)
|
||||||
case app.SessionSelectedMsg:
|
case app.SessionSelectedMsg:
|
||||||
|
var err error
|
||||||
a.app.Session = msg
|
a.app.Session = msg
|
||||||
a.app.Messages, _ = a.app.ListMessages(context.Background(), msg.Id)
|
a.app.Messages, err = a.app.ListMessages(context.Background(), msg.Id)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to list messages", "error", err)
|
||||||
|
}
|
||||||
case app.ModelSelectedMsg:
|
case app.ModelSelectedMsg:
|
||||||
a.app.Provider = &msg.Provider
|
a.app.Provider = &msg.Provider
|
||||||
a.app.Model = &msg.Model
|
a.app.Model = &msg.Model
|
||||||
|
|||||||
Reference in New Issue
Block a user