From 5302c4912cb64bc43f2d68a1f43cef62c8d3f665 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 10 Apr 2023 14:01:19 -0400 Subject: [PATCH] feat(ui): notify copied text Fixes: https://github.com/charmbracelet/soft-serve/issues/154 --- ui/common/utils.go | 4 +- ui/components/statusbar/statusbar.go | 36 +++++++++++------ ui/pages/repo/empty.go | 2 +- ui/pages/repo/files.go | 2 +- ui/pages/repo/filesitem.go | 4 +- ui/pages/repo/logitem.go | 9 +---- ui/pages/repo/refsitem.go | 4 +- ui/pages/repo/repo.go | 58 ++++++++++++++-------------- ui/pages/selection/item.go | 28 +++++++++----- ui/pages/selection/selection.go | 2 +- 10 files changed, 80 insertions(+), 69 deletions(-) diff --git a/ui/common/utils.go b/ui/common/utils.go index 119f1b787..97d498cb6 100644 --- a/ui/common/utils.go +++ b/ui/common/utils.go @@ -16,8 +16,8 @@ func TruncateString(s string, max int) string { return truncate.StringWithTail(s, uint(max), "…") } -// RepoURL returns the URL of the repository. -func RepoURL(publicURL, name string) string { +// CloneCmd returns the URL of the repository. +func CloneCmd(publicURL, name string) string { name = utils.SanitizeRepo(name) + ".git" url, err := url.Parse(publicURL) if err == nil { diff --git a/ui/components/statusbar/statusbar.go b/ui/components/statusbar/statusbar.go index 7b960569f..c208c8489 100644 --- a/ui/components/statusbar/statusbar.go +++ b/ui/components/statusbar/statusbar.go @@ -9,16 +9,19 @@ import ( // StatusBarMsg is a message sent to the status bar. type StatusBarMsg struct { - Key string - Value string - Info string - Branch string + Key string + Value string + Info string + Extra string } // StatusBar is a status bar model. type StatusBar struct { common common.Common - msg StatusBarMsg + key string + value string + info string + extra string } // Model is an interface that supports setting the status bar information. @@ -50,7 +53,18 @@ func (s *StatusBar) Init() tea.Cmd { func (s *StatusBar) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case StatusBarMsg: - s.msg = msg + if msg.Key != "" { + s.key = msg.Key + } + if msg.Value != "" { + s.value = msg.Value + } + if msg.Info != "" { + s.info = msg.Info + } + if msg.Extra != "" { + s.extra = msg.Extra + } } return s, nil } @@ -63,14 +77,14 @@ func (s *StatusBar) View() string { "repo-help", st.StatusBarHelp.Render("? Help"), ) - key := st.StatusBarKey.Render(s.msg.Key) + key := st.StatusBarKey.Render(s.key) info := "" - if s.msg.Info != "" { - info = st.StatusBarInfo.Render(s.msg.Info) + if s.info != "" { + info = st.StatusBarInfo.Render(s.info) } - branch := st.StatusBarBranch.Render(s.msg.Branch) + branch := st.StatusBarBranch.Render(s.extra) maxWidth := s.common.Width - w(key) - w(info) - w(branch) - w(help) - v := truncate.StringWithTail(s.msg.Value, uint(maxWidth-st.StatusBarValue.GetHorizontalFrameSize()), "…") + v := truncate.StringWithTail(s.value, uint(maxWidth-st.StatusBarValue.GetHorizontalFrameSize()), "…") value := st.StatusBarValue. Width(maxWidth). Render(v) diff --git a/ui/pages/repo/empty.go b/ui/pages/repo/empty.go index fe3921d9c..1e62e8f86 100644 --- a/ui/pages/repo/empty.go +++ b/ui/pages/repo/empty.go @@ -36,5 +36,5 @@ git push -u origin main git remote add origin %[1]s git push -u origin main `+"```"+` -`, common.RepoURL(cfg.SSH.PublicURL, repo)) +`, common.CloneCmd(cfg.SSH.PublicURL, repo)) } diff --git a/ui/pages/repo/files.go b/ui/pages/repo/files.go index edae23f1f..a281c1e87 100644 --- a/ui/pages/repo/files.go +++ b/ui/pages/repo/files.go @@ -243,7 +243,7 @@ func (f *Files) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, f.common.KeyMap.BackItem): cmds = append(cmds, backCmd) case key.Matches(msg, f.common.KeyMap.Copy): - f.common.Copy.Copy(f.currentContent.content) + cmds = append(cmds, copyCmd(f.currentContent.content, "File contents copied to clipboard")) case key.Matches(msg, lineNo): f.lineNumber = !f.lineNumber f.code.SetShowLineNumber(f.lineNumber) diff --git a/ui/pages/repo/filesitem.go b/ui/pages/repo/filesitem.go index 1d64cb8d1..eae17d920 100644 --- a/ui/pages/repo/filesitem.go +++ b/ui/pages/repo/filesitem.go @@ -78,7 +78,6 @@ func (d FileItemDelegate) Spacing() int { return 0 } // Update implements list.ItemDelegate. func (d FileItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { - idx := m.Index() item, ok := m.SelectedItem().(FileItem) if !ok { return nil @@ -87,8 +86,7 @@ func (d FileItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { case tea.KeyMsg: switch { case key.Matches(msg, d.common.KeyMap.Copy): - d.common.Copy.Copy(item.Title()) - return m.SetItem(idx, item) + return copyCmd(item.entry.Name(), fmt.Sprintf("File name %q copied to clipboard", item.entry.Name())) } } return nil diff --git a/ui/pages/repo/logitem.go b/ui/pages/repo/logitem.go index b30c21d67..06e27ea85 100644 --- a/ui/pages/repo/logitem.go +++ b/ui/pages/repo/logitem.go @@ -18,7 +18,6 @@ import ( // 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. @@ -57,7 +56,6 @@ 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 { - idx := m.Index() item, ok := m.SelectedItem().(LogItem) if !ok { return nil @@ -66,9 +64,7 @@ func (d LogItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { 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 copyCmd(item.Hash(), fmt.Sprintf("Commit hash %q copied to clipboard", item.Hash())) } } return nil @@ -92,9 +88,6 @@ func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l horizontalFrameSize := styles.Base.GetHorizontalFrameSize() hash := i.Commit.ID.String()[:7] - if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) { - hash = "copied" - } title := styles.Title.Render( common.TruncateString(i.Title(), m.Width()- diff --git a/ui/pages/repo/refsitem.go b/ui/pages/repo/refsitem.go index ec378696f..f7d8a1522 100644 --- a/ui/pages/repo/refsitem.go +++ b/ui/pages/repo/refsitem.go @@ -67,7 +67,6 @@ func (d RefItemDelegate) Spacing() int { return 0 } // Update implements list.ItemDelegate. func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { - idx := m.Index() item, ok := m.SelectedItem().(RefItem) if !ok { return nil @@ -76,8 +75,7 @@ func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { case tea.KeyMsg: switch { case key.Matches(msg, d.common.KeyMap.Copy): - d.common.Copy.Copy(item.Title()) - return m.SetItem(idx, item) + return copyCmd(item.ID(), fmt.Sprintf("Reference %q copied to clipboard", item.ID())) } } return nil diff --git a/ui/pages/repo/repo.go b/ui/pages/repo/repo.go index 6be90105f..38119d9d5 100644 --- a/ui/pages/repo/repo.go +++ b/ui/pages/repo/repo.go @@ -2,7 +2,6 @@ package repo import ( "fmt" - "time" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -56,9 +55,6 @@ type EmptyRepoMsg struct{} // CopyURLMsg is a message to copy the URL of the current repository. type CopyURLMsg struct{} -// ResetURLMsg is a message to reset the URL string. -type ResetURLMsg struct{} - // UpdateStatusBarMsg updates the status bar. type UpdateStatusBarMsg struct{} @@ -68,6 +64,12 @@ type RepoMsg backend.Repository // BackMsg is a message to go back to the previous view. type BackMsg struct{} +// CopyMsg is a message to indicate copied text. +type CopyMsg struct { + Text string + Message string +} + // Repo is a view for a git repository. type Repo struct { common common.Common @@ -77,7 +79,6 @@ type Repo struct { statusbar *statusbar.StatusBar panes []common.Component ref *git.Reference - copyURL time.Time state state spinner spinner.Model panesReady [lastTab]bool @@ -215,8 +216,9 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if r.selectedRepo != nil { cmds = append(cmds, r.updateStatusBarCmd) urlID := fmt.Sprintf("%s-url", r.selectedRepo.Name()) + cmd := common.CloneCmd(r.common.Config().SSH.PublicURL, r.selectedRepo.Name()) if msg, ok := msg.(tea.MouseMsg); ok && r.common.Zone.Get(urlID).InBounds(msg) { - cmds = append(cmds, r.copyURLCmd()) + cmds = append(cmds, copyCmd(cmd, "Command copied to clipboard")) } } switch msg := msg.(type) { @@ -234,14 +236,16 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } } - case CopyURLMsg: + case CopyMsg: + txt := msg.Text if cfg := r.common.Config(); cfg != nil { - r.common.Copy.Copy( - common.RepoURL(cfg.SSH.PublicURL, r.selectedRepo.Name()), - ) + r.common.Copy.Copy(txt) } - case ResetURLMsg: - r.copyURL = time.Time{} + cmds = append(cmds, func() tea.Msg { + return statusbar.StatusBarMsg{ + Value: msg.Message, + } + }) case ReadmeMsg, FileItemsMsg, LogCountMsg, LogItemsMsg, RefItemsMsg: cmds = append(cmds, r.updateRepo(msg)) // We have two spinners, one is used to when loading the repository and the @@ -345,10 +349,7 @@ func (r *Repo) headerView() string { Align(lipgloss.Right) var url string if cfg := r.common.Config(); cfg != nil { - url = common.RepoURL(cfg.SSH.PublicURL, r.selectedRepo.Name()) - } - if !r.copyURL.IsZero() && r.copyURL.Add(time.Second).After(time.Now()) { - url = "copied!" + url = common.CloneCmd(cfg.SSH.PublicURL, r.selectedRepo.Name()) } url = common.TruncateString(url, r.common.Width-lipgloss.Width(desc)-1) url = r.common.Zone.Mark( @@ -378,10 +379,10 @@ func (r *Repo) updateStatusBarCmd() tea.Msg { branch += " " + r.ref.Name().Short() } return statusbar.StatusBarMsg{ - Key: r.selectedRepo.Name(), - Value: value, - Info: info, - Branch: branch, + Key: r.selectedRepo.Name(), + Value: value, + Info: info, + Extra: branch, } } @@ -458,16 +459,13 @@ func (r *Repo) isReady() bool { return ready } -func (r *Repo) copyURLCmd() tea.Cmd { - r.copyURL = time.Now() - return tea.Batch( - func() tea.Msg { - return CopyURLMsg{} - }, - tea.Tick(time.Second, func(time.Time) tea.Msg { - return ResetURLMsg{} - }), - ) +func copyCmd(text, msg string) tea.Cmd { + return func() tea.Msg { + return CopyMsg{ + Text: text, + Message: msg, + } + } } func updateStatusBarCmd() tea.Msg { diff --git a/ui/pages/selection/item.go b/ui/pages/selection/item.go index 837ce69d3..7d24d5a66 100644 --- a/ui/pages/selection/item.go +++ b/ui/pages/selection/item.go @@ -51,7 +51,6 @@ type Item struct { repo backend.Repository lastUpdate *time.Time cmd string - copied time.Time } // New creates a new Item. @@ -64,7 +63,7 @@ func NewItem(repo backend.Repository, cfg *config.Config) (Item, error) { return Item{ repo: repo, lastUpdate: lastUpdate, - cmd: common.RepoURL(cfg.SSH.PublicURL, repo.Name()), + cmd: common.CloneCmd(cfg.SSH.PublicURL, repo.Name()), }, nil } @@ -98,6 +97,16 @@ func (i Item) Command() string { type ItemDelegate struct { common *common.Common activePane *pane + copiedIdx int +} + +// NewItemDelegate creates a new ItemDelegate. +func NewItemDelegate(common *common.Common, activePane *pane) *ItemDelegate { + return &ItemDelegate{ + common: common, + activePane: activePane, + copiedIdx: -1, + } } // Width returns the item width. @@ -107,16 +116,16 @@ func (d ItemDelegate) Width() int { } // Height returns the item height. Implements list.ItemDelegate. -func (d ItemDelegate) Height() int { +func (d *ItemDelegate) Height() int { height := d.common.Styles.MenuItem.GetVerticalFrameSize() + d.common.Styles.MenuItem.GetHeight() return height } // Spacing returns the spacing between items. Implements list.ItemDelegate. -func (d ItemDelegate) Spacing() int { return 1 } +func (d *ItemDelegate) Spacing() int { return 1 } // Update implements list.ItemDelegate. -func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { +func (d *ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { idx := m.Index() item, ok := m.SelectedItem().(Item) if !ok { @@ -126,7 +135,7 @@ func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { case tea.KeyMsg: switch { case key.Matches(msg, d.common.KeyMap.Copy): - item.copied = time.Now() + d.copiedIdx = idx d.common.Copy.Copy(item.Command()) return m.SetItem(idx, item) } @@ -135,7 +144,7 @@ func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { } // Render implements list.ItemDelegate. -func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { +func (d *ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { i := listItem.(Item) s := strings.Builder{} var matchedRunes []int @@ -192,8 +201,9 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list s.WriteRune('\n') cmd := common.TruncateString(i.Command(), m.Width()-styles.Base.GetHorizontalFrameSize()) cmd = styles.Command.Render(cmd) - if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) { - cmd = styles.Command.Render("Copied!") + if d.copiedIdx == index { + cmd += " " + styles.Desc.Render("(copied to clipboard)") + d.copiedIdx = -1 } s.WriteString(cmd) fmt.Fprint(w, diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index 036b74222..2c7957c6a 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -71,7 +71,7 @@ func New(c common.Common) *Selection { SetString(defaultNoContent) selector := selector.New(c, []selector.IdentifiableItem{}, - ItemDelegate{&c, &sel.activePane}) + NewItemDelegate(&c, &sel.activePane)) selector.SetShowTitle(false) selector.SetShowHelp(false) selector.SetShowStatusBar(false)