5302c4912cb64bc43f2d68a1f43cef62c8d3f665

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

feat(ui): notify copied text

Fixes: https://github.com/charmbracelet/soft-serve/issues/154

Diff

This diff is truncated to protect this page.

  1diff --git a/ui/common/utils.go b/ui/common/utils.go
  2index 119f1b787d953586d9723a4a38a8f39859015f0f..97d498cb63ca1b77e7565d60dd870b4aff9d7575 100644
  3--- a/ui/common/utils.go
  4+++ b/ui/common/utils.go
  5@@ -16,8 +16,8 @@ func TruncateString(s string, max int) string {
  6 	return truncate.StringWithTail(s, uint(max), "…")
  7 }
  8 
  9-// RepoURL returns the URL of the repository.
 10-func RepoURL(publicURL, name string) string {
 11+// CloneCmd returns the URL of the repository.
 12+func CloneCmd(publicURL, name string) string {
 13 	name = utils.SanitizeRepo(name) + ".git"
 14 	url, err := url.Parse(publicURL)
 15 	if err == nil {
 16diff --git a/ui/components/statusbar/statusbar.go b/ui/components/statusbar/statusbar.go
 17index 7b960569faf23f4668c2bfc3ddc3792cf73c9ad7..c208c84897731234ba3b5611b7751de2ad930e3c 100644
 18--- a/ui/components/statusbar/statusbar.go
 19+++ b/ui/components/statusbar/statusbar.go
 20@@ -9,16 +9,19 @@ import (
 21 
 22 // StatusBarMsg is a message sent to the status bar.
 23 type StatusBarMsg struct {
 24-	Key    string
 25-	Value  string
 26-	Info   string
 27-	Branch string
 28+	Key   string
 29+	Value string
 30+	Info  string
 31+	Extra string
 32 }
 33 
 34 // StatusBar is a status bar model.
 35 type StatusBar struct {
 36 	common common.Common
 37-	msg    StatusBarMsg
 38+	key    string
 39+	value  string
 40+	info   string
 41+	extra  string
 42 }
 43 
 44 // Model is an interface that supports setting the status bar information.
 45@@ -50,7 +53,18 @@ func (s *StatusBar) Init() tea.Cmd {
 46 func (s *StatusBar) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 47 	switch msg := msg.(type) {
 48 	case StatusBarMsg:
 49-		s.msg = msg
 50+		if msg.Key != "" {
 51+			s.key = msg.Key
 52+		}
 53+		if msg.Value != "" {
 54+			s.value = msg.Value
 55+		}
 56+		if msg.Info != "" {
 57+			s.info = msg.Info
 58+		}
 59+		if msg.Extra != "" {
 60+			s.extra = msg.Extra
 61+		}
 62 	}
 63 	return s, nil
 64 }
 65@@ -63,14 +77,14 @@ func (s *StatusBar) View() string {
 66 		"repo-help",
 67 		st.StatusBarHelp.Render("? Help"),
 68 	)
 69-	key := st.StatusBarKey.Render(s.msg.Key)
 70+	key := st.StatusBarKey.Render(s.key)
 71 	info := ""
 72-	if s.msg.Info != "" {
 73-		info = st.StatusBarInfo.Render(s.msg.Info)
 74+	if s.info != "" {
 75+		info = st.StatusBarInfo.Render(s.info)
 76 	}
 77-	branch := st.StatusBarBranch.Render(s.msg.Branch)
 78+	branch := st.StatusBarBranch.Render(s.extra)
 79 	maxWidth := s.common.Width - w(key) - w(info) - w(branch) - w(help)
 80-	v := truncate.StringWithTail(s.msg.Value, uint(maxWidth-st.StatusBarValue.GetHorizontalFrameSize()), "…")
 81+	v := truncate.StringWithTail(s.value, uint(maxWidth-st.StatusBarValue.GetHorizontalFrameSize()), "…")
 82 	value := st.StatusBarValue.
 83 		Width(maxWidth).
 84 		Render(v)
 85diff --git a/ui/pages/repo/empty.go b/ui/pages/repo/empty.go
 86index fe3921d9c66fe1d6619a412a33558b418fa41694..1e62e8f869988c8f2c525b03b2d67e24a384c42f 100644
 87--- a/ui/pages/repo/empty.go
 88+++ b/ui/pages/repo/empty.go
 89@@ -36,5 +36,5 @@ git push -u origin main
 90 git remote add origin %[1]s
 91 git push -u origin main
 92 `+"```"+`
 93-`, common.RepoURL(cfg.SSH.PublicURL, repo))
 94+`, common.CloneCmd(cfg.SSH.PublicURL, repo))
 95 }
 96diff --git a/ui/pages/repo/files.go b/ui/pages/repo/files.go
 97index edae23f1ff318e1ec2505d16edc809e035de38b7..a281c1e879c932daacae7c8491d9e3966687b4bb 100644
 98--- a/ui/pages/repo/files.go
 99+++ b/ui/pages/repo/files.go
100@@ -243,7 +243,7 @@ func (f *Files) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101 			case key.Matches(msg, f.common.KeyMap.BackItem):
102 				cmds = append(cmds, backCmd)
103 			case key.Matches(msg, f.common.KeyMap.Copy):
104-				f.common.Copy.Copy(f.currentContent.content)
105+				cmds = append(cmds, copyCmd(f.currentContent.content, "File contents copied to clipboard"))
106 			case key.Matches(msg, lineNo):
107 				f.lineNumber = !f.lineNumber
108 				f.code.SetShowLineNumber(f.lineNumber)
109diff --git a/ui/pages/repo/filesitem.go b/ui/pages/repo/filesitem.go
110index 1d64cb8d13e1ed7ba3262159dfc6686d6bc00ba0..eae17d920f4384bc8de0f65dc5ed9a0e9f575f43 100644
111--- a/ui/pages/repo/filesitem.go
112+++ b/ui/pages/repo/filesitem.go
113@@ -78,7 +78,6 @@ func (d FileItemDelegate) Spacing() int { return 0 }
114 
115 // Update implements list.ItemDelegate.
116 func (d FileItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
117-	idx := m.Index()
118 	item, ok := m.SelectedItem().(FileItem)
119 	if !ok {
120 		return nil
121@@ -87,8 +86,7 @@ func (d FileItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
122 	case tea.KeyMsg:
123 		switch {
124 		case key.Matches(msg, d.common.KeyMap.Copy):
125-			d.common.Copy.Copy(item.Title())
126-			return m.SetItem(idx, item)
127+			return copyCmd(item.entry.Name(), fmt.Sprintf("File name %q copied to clipboard", item.entry.Name()))
128 		}
129 	}
130 	return nil
131diff --git a/ui/pages/repo/logitem.go b/ui/pages/repo/logitem.go
132index b30c21d67f4b371c121b0007c5b93073811e078d..06e27ea858d5bccb43fde70f554bdecc3f8b86cd 100644
133--- a/ui/pages/repo/logitem.go
134+++ b/ui/pages/repo/logitem.go
135@@ -18,7 +18,6 @@ import (
136 // LogItem is a item in the log list that displays a git commit.
137 type LogItem struct {
138 	*git.Commit
139-	copied time.Time
140 }
141 
142 // ID implements selector.IdentifiableItem.
143@@ -57,7 +56,6 @@ func (d LogItemDelegate) Spacing() int { return 1 }
144 
145 // Update updates the item. Implements list.ItemDelegate.
146 func (d LogItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
147-	idx := m.Index()
148 	item, ok := m.SelectedItem().(LogItem)
149 	if !ok {
150 		return nil
151@@ -66,9 +64,7 @@ func (d LogItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
152 	case tea.KeyMsg:
153 		switch {
154 		case key.Matches(msg, d.common.KeyMap.Copy):
155-			item.copied = time.Now()
156-			d.common.Copy.Copy(item.Hash())
157-			return m.SetItem(idx, item)
158+			return copyCmd(item.Hash(), fmt.Sprintf("Commit hash %q copied to clipboard", item.Hash()))
159 		}
160 	}
161 	return nil
162@@ -92,9 +88,6 @@ func (d LogItemDelegate) Render(w io.Writer, m list.Model, index int, listItem l
163 	horizontalFrameSize := styles.Base.GetHorizontalFrameSize()
164 
165 	hash := i.Commit.ID.String()[:7]
166-	if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
167-		hash = "copied"
168-	}
169 	title := styles.Title.Render(
170 		common.TruncateString(i.Title(),
171 			m.Width()-
172diff --git a/ui/pages/repo/refsitem.go b/ui/pages/repo/refsitem.go
173index ec378696f450278914c13eadf51f25b1b458a95e..f7d8a15221d2df03d43df9daa2b401f5cf32dc04 100644
174--- a/ui/pages/repo/refsitem.go
175+++ b/ui/pages/repo/refsitem.go
176@@ -67,7 +67,6 @@ func (d RefItemDelegate) Spacing() int { return 0 }
177 
178 // Update implements list.ItemDelegate.
179 func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
180-	idx := m.Index()
181 	item, ok := m.SelectedItem().(RefItem)
182 	if !ok {
183 		return nil
184@@ -76,8 +75,7 @@ func (d RefItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
185 	case tea.KeyMsg:
186 		switch {
187 		case key.Matches(msg, d.common.KeyMap.Copy):
188-			d.common.Copy.Copy(item.Title())
189-			return m.SetItem(idx, item)
190+			return copyCmd(item.ID(), fmt.Sprintf("Reference %q copied to clipboard", item.ID()))
191 		}
192 	}
193 	return nil
194diff --git a/ui/pages/repo/repo.go b/ui/pages/repo/repo.go
195index 6be90105feea3b7fd650919882fe99dc618598a3..38119d9d520f74364ec2c5cbafce5b11e61b0263 100644
196--- a/ui/pages/repo/repo.go
197+++ b/ui/pages/repo/repo.go
198@@ -2,7 +2,6 @@ package repo
199 
200 import (
201 	"fmt"
202-	"time"
203 
204 	"github.com/charmbracelet/bubbles/help"
205 	"github.com/charmbracelet/bubbles/key"
206@@ -56,9 +55,6 @@ type EmptyRepoMsg struct{}
207 // CopyURLMsg is a message to copy the URL of the current repository.
208 type CopyURLMsg struct{}
209 
210-// ResetURLMsg is a message to reset the URL string.
211-type ResetURLMsg struct{}
212-
213 // UpdateStatusBarMsg updates the status bar.
214 type UpdateStatusBarMsg struct{}
215 
216@@ -68,6 +64,12 @@ type RepoMsg backend.Repository
217 // BackMsg is a message to go back to the previous view.
218 type BackMsg struct{}
219 
220+// CopyMsg is a message to indicate copied text.
221+type CopyMsg struct {
222+	Text    string
223+	Message string
224+}
225+
226 // Repo is a view for a git repository.
227 type Repo struct {
228 	common       common.Common
229@@ -77,7 +79,6 @@ type Repo struct {
230 	statusbar    *statusbar.StatusBar
231 	panes        []common.Component
232 	ref          *git.Reference
233-	copyURL      time.Time
234 	state        state
235 	spinner      spinner.Model
236 	panesReady   [lastTab]bool
237@@ -215,8 +216,9 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
238 		if r.selectedRepo != nil {
239 			cmds = append(cmds, r.updateStatusBarCmd)
240 			urlID := fmt.Sprintf("%s-url", r.selectedRepo.Name())
241+			cmd := common.CloneCmd(r.common.Config().SSH.PublicURL, r.selectedRepo.Name())
242 			if msg, ok := msg.(tea.MouseMsg); ok && r.common.Zone.Get(urlID).InBounds(msg) {
243-				cmds = append(cmds, r.copyURLCmd())
244+				cmds = append(cmds, copyCmd(cmd, "Command copied to clipboard"))
245 			}
246 		}
247 		switch msg := msg.(type) {
248@@ -234,14 +236,16 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
249 				}
250 			}
251 		}
252-	case CopyURLMsg:
253+	case CopyMsg:
254+		txt := msg.Text
255 		if cfg := r.common.Config(); cfg != nil {
256-			r.common.Copy.Copy(
257-				common.RepoURL(cfg.SSH.PublicURL, r.selectedRepo.Name()),
258-			)
259+			r.common.Copy.Copy(txt)
260 		}
261-	case ResetURLMsg:
262-		r.copyURL = time.Time{}
263+		cmds = append(cmds, func() tea.Msg {
264+			return statusbar.StatusBarMsg{
265+				Value: msg.Message,
266+			}
267+		})
268 	case ReadmeMsg, FileItemsMsg, LogCountMsg, LogItemsMsg, RefItemsMsg:
269 		cmds = append(cmds, r.updateRepo(msg))
270 	// We have two spinners, one is used to when loading the repository and the
271@@ -345,10 +349,7 @@ func (r *Repo) headerView() string {
272 		Align(lipgloss.Right)
273 	var url string
274 	if cfg := r.common.Config(); cfg != nil {
275-		url = common.RepoURL(cfg.SSH.PublicURL, r.selectedRepo.Name())
276-	}
277-	if !r.copyURL.IsZero() && r.copyURL.Add(time.Second).After(time.Now()) {
278-		url = "copied!"
279+		url = common.CloneCmd(cfg.SSH.PublicURL, r.selectedRepo.Name())
280 	}
281 	url = common.TruncateString(url, r.common.Width-lipgloss.Width(desc)-1)
282 	url = r.common.Zone.Mark(
283@@ -378,10 +379,10 @@ func (r *Repo) updateStatusBarCmd() tea.Msg {
284 		branch += " " + r.ref.Name().Short()
285 	}
286 	return statusbar.StatusBarMsg{
287-		Key:    r.selectedRepo.Name(),
288-		Value:  value,
289-		Info:   info,
290-		Branch: branch,
291+		Key:   r.selectedRepo.Name(),
292+		Value: value,
293+		Info:  info,
294+		Extra: branch,
295 	}
296 }
297 
298diff --git a/ui/pages/selection/item.go b/ui/pages/selection/item.go
299index 837ce69d37f5c4ea2c67def336e31d40f2d92a96..7d24d5a6647a899316e25994daef510fd0fb6bf8 100644
300--- a/ui/pages/selection/item.go
301+++ b/ui/pages/selection/item.go
302@@ -51,7 +51,6 @@ type Item struct {
303 	repo       backend.Repository
304 	lastUpdate *time.Time
305 	cmd        string
306-	copied     time.Time
307 }
308 
309 // New creates a new Item.
310@@ -64,7 +63,7 @@ func NewItem(repo backend.Repository, cfg *config.Config) (Item, error) {
311 	return Item{
312 		repo:       repo,
313 		lastUpdate: lastUpdate,
314-		cmd:        common.RepoURL(cfg.SSH.PublicURL, repo.Name()),
315+		cmd:        common.CloneCmd(cfg.SSH.PublicURL, repo.Name()),
316 	}, nil
317 }
318 
319@@ -98,6 +97,16 @@ func (i Item) Command() string {
320 type ItemDelegate struct {
321 	common     *common.Common
322 	activePane *pane
323+	copiedIdx  int
324+}
325+
326+// NewItemDelegate creates a new ItemDelegate.
327+func NewItemDelegate(common *common.Common, activePane *pane) *ItemDelegate {
328+	return &ItemDelegate{
329+		common:     common,
330+		activePane: activePane,
331+		copiedIdx:  -1,
332+	}
333 }
334 
335 // Width returns the item width.
336@@ -107,16 +116,16 @@ func (d ItemDelegate) Width() int {
337 }
338 
339 // Height returns the item height. Implements list.ItemDelegate.
340-func (d ItemDelegate) Height() int {
341+func (d *ItemDelegate) Height() int {
342 	height := d.common.Styles.MenuItem.GetVerticalFrameSize() + d.common.Styles.MenuItem.GetHeight()
343 	return height
344 }
345 
346 // Spacing returns the spacing between items. Implements list.ItemDelegate.
347-func (d ItemDelegate) Spacing() int { return 1 }
348+func (d *ItemDelegate) Spacing() int { return 1 }
349 
350 // Update implements list.ItemDelegate.
351-func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
352+func (d *ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
353 	idx := m.Index()
354 	item, ok := m.SelectedItem().(Item)
355 	if !ok {
356@@ -126,7 +135,7 @@ func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
357 	case tea.KeyMsg:
358 		switch {
359 		case key.Matches(msg, d.common.KeyMap.Copy):
360-			item.copied = time.Now()
361+			d.copiedIdx = idx
362 			d.common.Copy.Copy(item.Command())
363 			return m.SetItem(idx, item)
364 		}
365@@ -135,7 +144,7 @@ func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
366 }
367 
368 // Render implements list.ItemDelegate.
369-func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
370+func (d *ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
371 	i := listItem.(Item)
372 	s := strings.Builder{}
373 	var matchedRunes []int
374@@ -192,8 +201,9 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
375 	s.WriteRune('\n')
376 	cmd := common.TruncateString(i.Command(), m.Width()-styles.Base.GetHorizontalFrameSize())
377 	cmd = styles.Command.Render(cmd)
378-	if !i.copied.IsZero() && i.copied.Add(time.Second).After(time.Now()) {
379-		cmd = styles.Command.Render("Copied!")
380+	if d.copiedIdx == index {
381+		cmd += " " + styles.Desc.Render("(copied to clipboard)")
382+		d.copiedIdx = -1
383 	}
384 	s.WriteString(cmd)
385 	fmt.Fprint(w,
386diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go
387index 036b742227d5a030f0a6c2b90374f0b31964475e..2c7957c6aacaae4eb380dbc3447cf95de6b21580 100644
388--- a/ui/pages/selection/selection.go
389+++ b/ui/pages/selection/selection.go
390@@ -71,7 +71,7 @@ func New(c common.Common) *Selection {
391 		SetString(defaultNoContent)
392 	selector := selector.New(c,
393 		[]selector.IdentifiableItem{},
394-		ItemDelegate{&c, &sel.activePane})
395+		NewItemDelegate(&c, &sel.activePane))
396 	selector.SetShowTitle(false)
397 	selector.SetShowHelp(false)
398 	selector.SetShowStatusBar(false)