Detailed changes
@@ -5,7 +5,7 @@ go 1.17
require (
github.com/alecthomas/chroma v0.10.0
github.com/caarlos0/env/v6 v6.9.1
- github.com/charmbracelet/bubbles v0.10.4-0.20220412141214-292a1dd7ba97
+ github.com/charmbracelet/bubbles v0.10.4-0.20220429162018-2a8d463bd11f
github.com/charmbracelet/bubbletea v0.20.0
github.com/charmbracelet/glamour v0.4.0
github.com/charmbracelet/lipgloss v0.4.0
@@ -25,8 +25,8 @@ github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k
github.com/caarlos0/env/v6 v6.9.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3 h1:w2ANoiT4ubmh4Nssa3/QW1M7lj3FZkma8f8V5aBDxXM=
github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
-github.com/charmbracelet/bubbles v0.10.4-0.20220412141214-292a1dd7ba97 h1:NJqAUfS+JNHqodsbhLR0zD3sDkXI7skwjAwd77HXe/Q=
-github.com/charmbracelet/bubbles v0.10.4-0.20220412141214-292a1dd7ba97/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
+github.com/charmbracelet/bubbles v0.10.4-0.20220429162018-2a8d463bd11f h1:5mbyuBNzjF1S1pJOGubmjMBNUVO3NmjNsaZPIsFwPUQ=
+github.com/charmbracelet/bubbles v0.10.4-0.20220429162018-2a8d463bd11f/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
@@ -3,6 +3,7 @@ package server
import (
"fmt"
+ "github.com/aymanbagabas/go-osc52"
tea "github.com/charmbracelet/bubbletea"
appCfg "github.com/charmbracelet/soft-serve/config"
"github.com/charmbracelet/soft-serve/ui"
@@ -60,7 +61,11 @@ func SessionHandler(ac *appCfg.Config) bm.ProgramHandler {
if ac.Cfg.Callbacks != nil {
ac.Cfg.Callbacks.Tui("new session")
}
+ envs := s.Environ()
+ envs = append(envs, fmt.Sprintf("TERM=%s", pty.Term))
+ output := osc52.NewOutput(s, envs)
c := common.Common{
+ Copy: output,
Styles: styles.DefaultStyles(),
KeyMap: keymap.DefaultKeyMap(),
Width: pty.Window.Width,
@@ -1,12 +1,14 @@
package common
import (
+ "github.com/aymanbagabas/go-osc52"
"github.com/charmbracelet/soft-serve/ui/keymap"
"github.com/charmbracelet/soft-serve/ui/styles"
)
// Common is a struct all components should embed.
type Common struct {
+ Copy *osc52.Output
Styles *styles.Styles
KeyMap *keymap.KeyMap
Width int
@@ -2,6 +2,7 @@ package code
import (
"strings"
+ "sync"
"github.com/alecthomas/chroma/lexers"
tea "github.com/charmbracelet/bubbletea"
@@ -20,6 +21,9 @@ type Code struct {
common common.Common
content string
extension string
+ renderContext gansi.RenderContext
+ renderMutex sync.Mutex
+ styleConfig gansi.StyleConfig
NoContentStyle lipgloss.Style
}
@@ -32,6 +36,12 @@ func New(c common.Common, content, extension string) *Code {
Viewport: vp.New(c),
NoContentStyle: c.Styles.CodeNoContent.Copy(),
}
+ st := styleConfig()
+ r.styleConfig = st
+ r.renderContext = gansi.NewRenderContext(gansi.Options{
+ ColorProfile: termenv.TrueColor,
+ Styles: st,
+ })
r.SetSize(c.Width, c.Height)
return r
}
@@ -56,7 +66,7 @@ func (r *Code) Init() tea.Cmd {
if c == "" {
c = r.NoContentStyle.String()
}
- f, err := renderFile(r.extension, c, w)
+ f, err := r.renderFile(r.extension, c, w)
if err != nil {
return common.ErrorCmd(err)
}
@@ -136,30 +146,14 @@ func (r *Code) ScrollPercent() float64 {
return r.Viewport.ScrollPercent()
}
-func styleConfig() gansi.StyleConfig {
- noColor := ""
- s := glamour.DarkStyleConfig
- // This fixes an issue with the default style config. For example
- // highlighting empty spaces with red in Dockerfile type.
- s.Document.StylePrimitive.Color = &noColor
- s.CodeBlock.Chroma.Text.Color = &noColor
- s.CodeBlock.Chroma.Name.Color = &noColor
- return s
-}
-
-func renderCtx() gansi.RenderContext {
- return gansi.NewRenderContext(gansi.Options{
- ColorProfile: termenv.TrueColor,
- Styles: styleConfig(),
- })
-}
-
-func glamourize(w int, md string) (string, error) {
+func (r *Code) glamourize(w int, md string) (string, error) {
+ r.renderMutex.Lock()
+ defer r.renderMutex.Unlock()
if w > 120 {
w = 120
}
tr, err := glamour.NewTermRenderer(
- glamour.WithStyles(styleConfig()),
+ glamour.WithStyles(r.styleConfig),
glamour.WithWordWrap(w),
)
@@ -173,7 +167,7 @@ func glamourize(w int, md string) (string, error) {
return mdt, nil
}
-func renderFile(path, content string, width int) (string, error) {
+func (r *Code) renderFile(path, content string, width int) (string, error) {
lexer := lexers.Fallback
if path == "" {
lexer = lexers.Analyse(content)
@@ -185,7 +179,7 @@ func renderFile(path, content string, width int) (string, error) {
lang = lexer.Config().Name
}
if lang == "markdown" {
- md, err := glamourize(width, content)
+ md, err := r.glamourize(width, content)
if err != nil {
return "", err
}
@@ -195,10 +189,21 @@ func renderFile(path, content string, width int) (string, error) {
Code: content,
Language: lang,
}
- r := strings.Builder{}
- err := formatter.Render(&r, renderCtx())
+ s := strings.Builder{}
+ err := formatter.Render(&s, r.renderContext)
if err != nil {
return "", err
}
- return r.String(), nil
+ return s.String(), nil
+}
+
+func styleConfig() gansi.StyleConfig {
+ noColor := ""
+ s := glamour.DarkStyleConfig
+ // This fixes an issue with the default style config. For example
+ // highlighting empty spaces with red in Dockerfile type.
+ s.Document.StylePrimitive.Color = &noColor
+ s.CodeBlock.Chroma.Text.Color = &noColor
+ s.CodeBlock.Chroma.Name.Color = &noColor
+ return s
}
@@ -0,0 +1,64 @@
+package copy
+
+import (
+ "github.com/aymanbagabas/go-osc52"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// CopyMsg is a message that is sent when the user copies text.
+type CopyMsg string
+
+// CopyCmd is a command that copies text to the clipboard using OSC52.
+func CopyCmd(output *osc52.Output, str string) tea.Cmd {
+ return func() tea.Msg {
+ output.Copy(str)
+ return CopyMsg(str)
+ }
+}
+
+type Copy struct {
+ output *osc52.Output
+ text string
+ copied bool
+ CopiedStyle lipgloss.Style
+ TextStyle lipgloss.Style
+}
+
+func New(output *osc52.Output, text string) *Copy {
+ copy := &Copy{
+ output: output,
+ text: text,
+ }
+ return copy
+}
+
+func (c *Copy) SetText(text string) {
+ c.text = text
+}
+
+func (c *Copy) Init() tea.Cmd {
+ c.copied = false
+ return nil
+}
+
+func (c *Copy) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg.(type) {
+ case CopyMsg:
+ c.copied = true
+ default:
+ c.copied = false
+ }
+ return c, nil
+}
+
+func (c *Copy) View() string {
+ if c.copied {
+ return c.CopiedStyle.String()
+ }
+ return c.TextStyle.Render(c.text)
+}
+
+func (c *Copy) CopyCmd() tea.Cmd {
+ return CopyCmd(c.output, c.text)
+}
@@ -1,7 +1,6 @@
package viewport
import (
- "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/soft-serve/ui/common"
@@ -17,32 +16,6 @@ type Viewport struct {
func New(c common.Common) *Viewport {
vp := viewport.New(c.Width, c.Height)
vp.MouseWheelEnabled = true
- vp.KeyMap = viewport.KeyMap{
- PageDown: key.NewBinding(
- key.WithKeys("pgdown", " ", "f"),
- key.WithHelp("f/pgdn", "page down"),
- ),
- PageUp: key.NewBinding(
- key.WithKeys("pgup", "b"),
- key.WithHelp("b/pgup", "page up"),
- ),
- HalfPageUp: key.NewBinding(
- key.WithKeys("u", "ctrl+u"),
- key.WithHelp("ctrl+u/u", "half page up"),
- ),
- HalfPageDown: key.NewBinding(
- key.WithKeys("d", "ctrl+d"),
- key.WithHelp("ctrl+d/d", "half page down"),
- ),
- Up: key.NewBinding(
- key.WithKeys("up", "k"),
- key.WithHelp("↑/k", "up"),
- ),
- Down: key.NewBinding(
- key.WithKeys("down", "j"),
- key.WithHelp("↓/j", "down"),
- ),
- }
return &Viewport{
common: c,
Model: &vp,
@@ -19,6 +19,8 @@ type KeyMap struct {
SelectItem key.Binding
BackItem key.Binding
+
+ Copy key.Binding
}
// DefaultKeyMap returns the default key map.
@@ -188,5 +190,16 @@ func DefaultKeyMap() *KeyMap {
),
)
+ km.Copy = key.NewBinding(
+ key.WithKeys(
+ "c",
+ "ctrl+c",
+ ),
+ key.WithHelp(
+ "c",
+ "copy text",
+ ),
+ )
+
return km
}
@@ -3,7 +3,6 @@ package repo
import (
"errors"
"fmt"
- "log"
"path/filepath"
"github.com/charmbracelet/bubbles/key"
@@ -60,7 +59,7 @@ func NewFiles(common common.Common) *Files {
activeView: filesViewFiles,
lastSelected: make([]int, 0),
}
- selector := selector.New(common, []selector.IdentifiableItem{}, FileItemDelegate{common.Styles})
+ selector := selector.New(common, []selector.IdentifiableItem{}, FileItemDelegate{&common})
selector.SetShowFilter(false)
selector.SetShowHelp(false)
selector.SetShowPagination(false)
@@ -86,16 +85,22 @@ func (f *Files) ShortHelp() []key.Binding {
k := f.selector.KeyMap
switch f.activeView {
case filesViewFiles:
+ copyKey := f.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy name")
return []key.Binding{
f.common.KeyMap.SelectItem,
f.common.KeyMap.BackItem,
k.CursorUp,
k.CursorDown,
+ copyKey,
}
case filesViewContent:
+ copyKey := f.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy content")
return []key.Binding{
f.common.KeyMap.UpDown,
f.common.KeyMap.BackItem,
+ copyKey,
}
default:
return []key.Binding{}
@@ -107,13 +112,14 @@ func (f *Files) FullHelp() [][]key.Binding {
b := make([][]key.Binding, 0)
switch f.activeView {
case filesViewFiles:
+ copyKey := f.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy name")
k := f.selector.KeyMap
b = append(b, []key.Binding{
f.common.KeyMap.SelectItem,
f.common.KeyMap.BackItem,
})
b = append(b, [][]key.Binding{
- {},
{
k.CursorUp,
k.CursorDown,
@@ -126,8 +132,13 @@ func (f *Files) FullHelp() [][]key.Binding {
k.GoToStart,
k.GoToEnd,
},
+ {
+ copyKey,
+ },
}...)
case filesViewContent:
+ copyKey := f.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy content")
k := f.code.KeyMap
b = append(b, []key.Binding{
f.common.KeyMap.BackItem,
@@ -145,6 +156,9 @@ func (f *Files) FullHelp() [][]key.Binding {
k.Down,
k.Up,
},
+ {
+ copyKey,
+ },
}...)
}
return b
@@ -186,7 +200,6 @@ func (f *Files) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case FileItem:
f.currentItem = &sel
f.path = filepath.Join(f.path, sel.entry.Name())
- log.Printf("selected index %d", f.selector.Index())
if sel.entry.IsTree() {
cmds = append(cmds, f.selectTreeCmd)
} else {
@@ -203,9 +216,12 @@ func (f *Files) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, f.deselectItemCmd)
}
case filesViewContent:
- switch msg.String() {
- case "h", "left":
+ keyStr := msg.String()
+ switch {
+ case keyStr == "h", keyStr == "left":
cmds = append(cmds, f.deselectItemCmd)
+ case key.Matches(msg, f.common.KeyMap.Copy):
+ f.common.Copy.Copy(f.currentContent.content)
}
}
case tea.WindowSizeMsg:
@@ -288,9 +304,9 @@ func (f *Files) updateFilesCmd() tea.Msg {
ents.Sort()
for _, e := range ents {
if e.IsTree() {
- dirs = append(dirs, FileItem{e})
+ dirs = append(dirs, FileItem{entry: e})
} else {
- files = append(files, FileItem{e})
+ files = append(files, FileItem{entry: e})
}
}
return FileItemsMsg(append(dirs, files...))
@@ -341,7 +357,6 @@ func (f *Files) deselectItemCmd() tea.Msg {
index = f.lastSelected[len(f.lastSelected)-1]
f.lastSelected = f.lastSelected[:len(f.lastSelected)-1]
}
- log.Printf("deselect %d", index)
f.selector.Select(index)
return msg
}
@@ -5,11 +5,12 @@ import (
"io"
"io/fs"
+ "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/ui/styles"
+ "github.com/charmbracelet/soft-serve/ui/common"
"github.com/dustin/go-humanize"
)
@@ -65,7 +66,7 @@ func (cl FileItems) Less(i, j int) bool {
// FileItemDelegate is the delegate for the file item list.
type FileItemDelegate struct {
- style *styles.Styles
+ common *common.Common
}
// Height returns the height of the file item list. Implements list.ItemDelegate.
@@ -75,11 +76,26 @@ func (d FileItemDelegate) Height() int { return 1 }
func (d FileItemDelegate) Spacing() int { return 0 }
// Update implements list.ItemDelegate.
-func (d FileItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
+func (d FileItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
+ idx := m.Index()
+ item, ok := m.SelectedItem().(FileItem)
+ if !ok {
+ return nil
+ }
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, d.common.KeyMap.Copy):
+ d.common.Copy.Copy(item.Title())
+ return m.SetItem(idx, item)
+ }
+ }
+ return nil
+}
// Render implements list.ItemDelegate.
func (d FileItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
- s := d.style
+ s := d.common.Styles
i, ok := listItem.(FileItem)
if !ok {
return
@@ -60,7 +60,7 @@ func NewLog(common common.Common) *Log {
vp: viewport.New(common),
activeView: logViewCommits,
}
- selector := selector.New(common, []selector.IdentifiableItem{}, LogItemDelegate{common.Styles})
+ selector := selector.New(common, []selector.IdentifiableItem{}, LogItemDelegate{&common})
selector.SetShowFilter(false)
selector.SetShowHelp(false)
selector.SetShowPagination(false)
@@ -85,8 +85,11 @@ func (l *Log) SetSize(width, height int) {
func (l *Log) ShortHelp() []key.Binding {
switch l.activeView {
case logViewCommits:
+ copyKey := l.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy hash")
return []key.Binding{
l.common.KeyMap.SelectItem,
+ copyKey,
}
case logViewDiff:
return []key.Binding{
@@ -104,11 +107,16 @@ func (l *Log) FullHelp() [][]key.Binding {
b := make([][]key.Binding, 0)
switch l.activeView {
case logViewCommits:
+ copyKey := l.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy hash")
b = append(b, []key.Binding{
l.common.KeyMap.SelectItem,
l.common.KeyMap.BackItem,
})
b = append(b, [][]key.Binding{
+ {
+ copyKey,
+ },
{
k.CursorUp,
k.CursorDown,
@@ -326,7 +334,7 @@ func (l *Log) updateCommitsCmd() tea.Msg {
if int64(idx) >= count {
break
}
- items[idx] = LogItem{c}
+ items[idx] = LogItem{Commit: c}
}
return LogItemsMsg(items)
}
@@ -6,21 +6,27 @@ import (
"strings"
"time"
+ "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/ui/styles"
+ "github.com/charmbracelet/soft-serve/ui/common"
"github.com/muesli/reflow/truncate"
)
// LogItem is a item in the log list that displays a git commit.
type LogItem struct {
*git.Commit
+ copied time.Time
}
// ID implements selector.IdentifiableItem.
func (i LogItem) ID() string {
+ return i.Hash()
+}
+
+func (i LogItem) Hash() string {
return i.Commit.ID.String()
}
@@ -40,7 +46,7 @@ func (i LogItem) FilterValue() string { return i.Title() }
// LogItemDelegate is the delegate for LogItem.
type LogItemDelegate struct {
- style *styles.Styles
+ common *common.Common
}
// Height returns the item height. Implements list.ItemDelegate.
@@ -50,10 +56,27 @@ func (d LogItemDelegate) Height() int { return 2 }
func (d LogItemDelegate) Spacing() int { return 1 }
// Update updates the item. Implements list.ItemDelegate.
-func (d LogItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
+func (d LogItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
+ idx := m.Index()
+ item, ok := m.SelectedItem().(LogItem)
+ if !ok {
+ return nil
+ }
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, d.common.KeyMap.Copy):
+ item.copied = time.Now()
+ d.common.Copy.Copy(item.Hash())
+ return m.SetItem(idx, item)
+ }
+ }
+ return nil
+}
// Render renders the item. Implements list.ItemDelegate.
func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
+ styles := d.common.Styles
i, ok := listItem.(LogItem)
if !ok {
return
@@ -63,17 +86,20 @@ func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
}
width := lipgloss.Width
- titleStyle := d.style.LogItemTitle.Copy()
- style := d.style.LogItemInactive
+ titleStyle := styles.LogItemTitle.Copy()
+ style := styles.LogItemInactive
if index == m.Index() {
titleStyle.Bold(true)
- style = d.style.LogItemActive
+ style = styles.LogItemActive
}
hash := " " + i.Commit.ID.String()[:7]
+ if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
+ hash = "copied"
+ }
title := titleStyle.Render(
truncateString(i.Title(), m.Width()-style.GetHorizontalFrameSize()-width(hash)-2, "…"),
)
- hash = d.style.LogItemHash.Copy().
+ hash = styles.LogItemHash.Copy().
Align(lipgloss.Right).
Width(m.Width() -
style.GetHorizontalFrameSize() -
@@ -108,23 +134,6 @@ func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
),
),
)
-
- // leftMargin := d.style.LogItemSelector.GetMarginLeft() +
- // d.style.LogItemSelector.GetWidth() +
- // d.style.LogItemHash.GetMarginLeft() +
- // d.style.LogItemHash.GetWidth() +
- // d.style.LogItemInactive.GetMarginLeft()
- // title := truncateString(i.Title(), m.Width()-leftMargin, "…")
- // if index == m.Index() {
- // fmt.Fprint(w, d.style.LogItemSelector.Render(">")+
- // d.style.LogItemHash.Bold(true).Render(hash[:7])+
- // d.style.LogItemActive.Render(title))
- // } else {
- // fmt.Fprint(w, d.style.LogItemSelector.Render(" ")+
- // d.style.LogItemHash.Render(hash[:7])+
- // d.style.LogItemInactive.Render(title))
- // }
- // fmt.Fprintln(w)
}
func truncateString(s string, max int, tail string) string {
@@ -35,7 +35,7 @@ func NewRefs(common common.Common, refPrefix string) *Refs {
common: common,
refPrefix: refPrefix,
}
- s := selector.New(common, []selector.IdentifiableItem{}, RefItemDelegate{common.Styles})
+ s := selector.New(common, []selector.IdentifiableItem{}, RefItemDelegate{&common})
s.SetShowFilter(false)
s.SetShowHelp(false)
s.SetShowPagination(true)
@@ -55,16 +55,21 @@ func (r *Refs) SetSize(width, height int) {
// ShortHelp implements help.KeyMap.
func (r *Refs) ShortHelp() []key.Binding {
+ copyKey := r.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy ref")
k := r.selector.KeyMap
return []key.Binding{
r.common.KeyMap.SelectItem,
k.CursorUp,
k.CursorDown,
+ copyKey,
}
}
// FullHelp implements help.KeyMap.
func (r *Refs) FullHelp() [][]key.Binding {
+ copyKey := r.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy ref")
k := r.selector.KeyMap
return [][]key.Binding{
{r.common.KeyMap.SelectItem},
@@ -80,6 +85,9 @@ func (r *Refs) FullHelp() [][]key.Binding {
k.GoToStart,
k.GoToEnd,
},
+ {
+ copyKey,
+ },
}
}
@@ -159,7 +167,7 @@ func (r *Refs) updateItemsCmd() tea.Msg {
}
for _, ref := range refs {
if strings.HasPrefix(ref.Name().String(), r.refPrefix) {
- its = append(its, RefItem{ref})
+ its = append(its, RefItem{Reference: ref})
}
}
sort.Sort(its)
@@ -4,11 +4,11 @@ import (
"fmt"
"io"
+ "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/tui/common"
- "github.com/charmbracelet/soft-serve/ui/styles"
+ "github.com/charmbracelet/soft-serve/ui/common"
)
// RefItem is a git reference item.
@@ -55,7 +55,7 @@ func (cl RefItems) Less(i, j int) bool {
// RefItemDelegate is the delegate for the ref item.
type RefItemDelegate struct {
- style *styles.Styles
+ common *common.Common
}
// Height implements list.ItemDelegate.
@@ -65,11 +65,26 @@ func (d RefItemDelegate) Height() int { return 1 }
func (d RefItemDelegate) Spacing() int { return 0 }
// Update implements list.ItemDelegate.
-func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
+func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
+ idx := m.Index()
+ item, ok := m.SelectedItem().(RefItem)
+ if !ok {
+ return nil
+ }
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, d.common.KeyMap.Copy):
+ d.common.Copy.Copy(item.Title())
+ return m.SetItem(idx, item)
+ }
+ }
+ return nil
+}
// Render implements list.ItemDelegate.
func (d RefItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
- s := d.style
+ s := d.common.Styles
i, ok := listItem.(RefItem)
if !ok {
return
@@ -84,12 +99,12 @@ func (d RefItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
s.RefItemSelector.GetMarginLeft() -
s.RefItemSelector.GetWidth() -
s.RefItemInactive.GetMarginLeft()
- ref = common.TruncateString(ref, refMaxWidth, "…")
+ ref = truncateString(ref, refMaxWidth, "…")
if index == m.Index() {
fmt.Fprint(w, s.RefItemSelector.Render(">")+
s.RefItemActive.Render(ref))
} else {
- fmt.Fprint(w, s.LogItemSelector.Render(" ")+
+ fmt.Fprint(w, s.RefItemSelector.Render(" ")+
s.RefItemInactive.Render(ref))
}
}
@@ -112,7 +112,7 @@ func (r *Repo) ShortHelp() []key.Binding {
case readmeTab:
b = append(b, r.common.KeyMap.UpDown)
default:
- b = append(b, r.boxes[commitsTab].(help.KeyMap).ShortHelp()...)
+ b = append(b, r.boxes[r.activeTab].(help.KeyMap).ShortHelp()...)
}
return b
}
@@ -6,12 +6,12 @@ import (
"strings"
"time"
+ "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/ui/components/yankable"
+ "github.com/charmbracelet/soft-serve/ui/common"
"github.com/charmbracelet/soft-serve/ui/git"
- "github.com/charmbracelet/soft-serve/ui/styles"
"github.com/dustin/go-humanize"
)
@@ -19,7 +19,8 @@ import (
type Item struct {
repo git.GitRepo
lastUpdate time.Time
- url *yankable.Yankable
+ cmd string
+ copied time.Time
}
// ID implements selector.IdentifiableItem.
@@ -36,26 +37,26 @@ func (i Item) Description() string { return i.repo.Description() }
// FilterValue implements list.Item.
func (i Item) FilterValue() string { return i.Title() }
-// URL returns the item URL view.
-func (i Item) URL() string {
- return i.url.View()
+// Command returns the item Command view.
+func (i Item) Command() string {
+ return i.cmd
}
// ItemDelegate is the delegate for the item.
type ItemDelegate struct {
- styles *styles.Styles
+ common *common.Common
activeBox *box
}
// Width returns the item width.
func (d ItemDelegate) Width() int {
- width := d.styles.MenuItem.GetHorizontalFrameSize() + d.styles.MenuItem.GetWidth()
+ width := d.common.Styles.MenuItem.GetHorizontalFrameSize() + d.common.Styles.MenuItem.GetWidth()
return width
}
// Height returns the item height. Implements list.ItemDelegate.
func (d ItemDelegate) Height() int {
- height := d.styles.MenuItem.GetVerticalFrameSize() + d.styles.MenuItem.GetHeight()
+ height := d.common.Styles.MenuItem.GetVerticalFrameSize() + d.common.Styles.MenuItem.GetHeight()
return height
}
@@ -64,40 +65,26 @@ func (d ItemDelegate) Spacing() int { return 0 }
// Update implements list.ItemDelegate.
func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
- cmds := make([]tea.Cmd, 0)
- // if d.activeBox == nil || *d.activeBox != selectorBox {
- // return nil
- // }
- for i, item := range m.VisibleItems() {
- itm, ok := item.(Item)
- if !ok {
- continue
- }
- // FIXME check if X & Y are within the item box
- switch msg := msg.(type) {
- case tea.MouseMsg:
- // x := msg.X
- y := msg.Y
- // minX := (i * d.Width())
- // maxX := minX + d.Width()
- minY := (i * d.Height())
- maxY := minY + d.Height()
- // log.Printf("i: %d, x: %d, y: %d", i, x, y)
- if y < minY || y > maxY {
- continue
- }
- }
- y, cmd := itm.url.Update(msg)
- itm.url = y.(*yankable.Yankable)
- if cmd != nil {
- cmds = append(cmds, cmd)
+ idx := m.Index()
+ item, ok := m.SelectedItem().(Item)
+ if !ok {
+ return nil
+ }
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, d.common.KeyMap.Copy):
+ item.copied = time.Now()
+ d.common.Copy.Copy(item.Command())
+ return m.SetItem(idx, item)
}
}
- return tea.Batch(cmds...)
+ return nil
}
// Render implements list.ItemDelegate.
func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
+ styles := d.common.Styles
i := listItem.(Item)
s := strings.Builder{}
var matchedRunes []int
@@ -105,13 +92,12 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
// Conditions
var (
isSelected = index == m.Index()
- // emptyFilter = m.FilterState() == list.Filtering && m.FilterValue() == ""
isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
)
- itemStyle := d.styles.MenuItem.Copy()
+ itemStyle := styles.MenuItem.Copy()
if isSelected {
- itemStyle = itemStyle.BorderForeground(d.styles.ActiveBorderColor)
+ itemStyle = itemStyle.BorderForeground(styles.ActiveBorderColor)
if d.activeBox != nil && *d.activeBox == readmeBox {
// TODO make this into its own color
itemStyle = itemStyle.BorderForeground(lipgloss.Color("15"))
@@ -120,7 +106,7 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
title := i.Title()
updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.lastUpdate))
- updated := d.styles.MenuLastUpdate.
+ updated := styles.MenuLastUpdate.
Copy().
Width(m.Width() - itemStyle.GetHorizontalFrameSize() - lipgloss.Width(title)).
Render(updatedStr)
@@ -144,6 +130,11 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
s.WriteString("\n")
s.WriteString(i.Description())
s.WriteString("\n\n")
- s.WriteString(i.url.View())
+ cmdStyle := styles.RepoCommand.Copy()
+ cmd := cmdStyle.Render(i.Command())
+ if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
+ cmd = cmdStyle.Render("Copied!")
+ }
+ s.WriteString(cmd)
w.Write([]byte(itemStyle.Render(s.String())))
}
@@ -1,16 +1,12 @@
package selection
import (
- "fmt"
- "strings"
-
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/soft-serve/ui/common"
"github.com/charmbracelet/soft-serve/ui/components/code"
"github.com/charmbracelet/soft-serve/ui/components/selector"
- "github.com/charmbracelet/soft-serve/ui/components/yankable"
"github.com/charmbracelet/soft-serve/ui/git"
"github.com/charmbracelet/soft-serve/ui/session"
)
@@ -42,7 +38,7 @@ func New(s session.Session, common common.Common) *Selection {
readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
selector := selector.New(common,
[]selector.IdentifiableItem{},
- ItemDelegate{common.Styles, &sel.activeBox})
+ ItemDelegate{&common, &sel.activeBox})
selector.SetShowTitle(false)
selector.SetShowHelp(false)
selector.SetShowStatusBar(false)
@@ -77,10 +73,13 @@ func (s *Selection) ShortHelp() []key.Binding {
s.common.KeyMap.Section,
)
if s.activeBox == selectorBox {
+ copyKey := s.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy command")
kb = append(kb,
s.common.KeyMap.Select,
k.Filter,
k.ClearFilter,
+ copyKey,
)
}
return kb
@@ -106,10 +105,13 @@ func (s *Selection) FullHelp() [][]key.Binding {
},
}
case selectorBox:
+ copyKey := s.common.KeyMap.Copy
+ copyKey.SetHelp("c", "copy command")
k := s.selector.KeyMap
return [][]key.Binding{
{
s.common.KeyMap.Select,
+ copyKey,
},
{
k.CursorUp,
@@ -136,32 +138,8 @@ func (s *Selection) FullHelp() [][]key.Binding {
// Init implements tea.Model.
func (s *Selection) Init() tea.Cmd {
- session := s.s.Session()
- environ := session.Environ()
- termExists := false
- // Add TERM using pty.Term if it's not already set.
- for _, env := range environ {
- if strings.HasPrefix(env, "TERM=") {
- termExists = true
- break
- }
- }
- if !termExists {
- pty, _, _ := session.Pty()
- environ = append(environ, fmt.Sprintf("TERM=%s", pty.Term))
- }
items := make([]selector.IdentifiableItem, 0)
cfg := s.s.Config()
- // TODO clean up this and move style to its own var.
- yank := func(text string) *yankable.Yankable {
- return yankable.New(
- session,
- environ,
- lipgloss.NewStyle().Foreground(lipgloss.Color("168")),
- lipgloss.NewStyle().Foreground(lipgloss.Color("168")).SetString("Copied!"),
- text,
- )
- }
// Put configured repos first
for _, r := range cfg.Repos {
repo, err := cfg.Source.GetRepo(r.Repo)
@@ -170,7 +148,7 @@ func (s *Selection) Init() tea.Cmd {
}
items = append(items, Item{
repo: repo,
- url: yank(git.RepoURL(cfg.Host, cfg.Port, r.Repo)),
+ cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo),
})
}
for _, r := range cfg.Source.AllRepos() {
@@ -196,7 +174,7 @@ func (s *Selection) Init() tea.Cmd {
items = append(items, Item{
repo: r,
lastUpdate: lastUpdate,
- url: yank(git.RepoURL(cfg.Host, cfg.Port, r.Name())),
+ cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()),
})
}
}
@@ -31,7 +31,7 @@ type Styles struct {
Repo lipgloss.Style
RepoTitle lipgloss.Style
RepoTitleBox lipgloss.Style
- RepoNote lipgloss.Style
+ RepoCommand lipgloss.Style
RepoNoteBox lipgloss.Style
RepoBody lipgloss.Style
RepoHeader lipgloss.Style
@@ -183,8 +183,7 @@ func DefaultStyles() *Styles {
BorderStyle(s.RepoTitleBorder).
BorderForeground(s.InactiveBorderColor)
- s.RepoNote = lipgloss.NewStyle().
- Padding(0, 2).
+ s.RepoCommand = lipgloss.NewStyle().
Foreground(lipgloss.Color("168"))
s.RepoNoteBox = lipgloss.NewStyle().
@@ -291,11 +290,15 @@ func DefaultStyles() *Styles {
Margin(0).
Align(lipgloss.Center)
- s.RefItemSelector = s.TreeItemSelector.Copy()
+ s.RefItemInactive = lipgloss.NewStyle().
+ MarginLeft(1)
- s.RefItemActive = s.TreeItemActive.Copy()
+ s.RefItemSelector = lipgloss.NewStyle().
+ Width(1).
+ Foreground(lipgloss.Color("#B083EA"))
- s.RefItemInactive = s.TreeItemInactive.Copy()
+ s.RefItemActive = s.RefItemInactive.Copy().
+ Bold(true)
s.RefItemBranch = lipgloss.NewStyle()