Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,38 +475,62 @@ func (m Model) isTypingState() bool {
}

// handleDepositKeys handles the keys that act on the deposit panel —
// arrow nav, enter-to-copy, q (fullscreen QR), g (image mode), c/C
// arrow nav, enter-to-copy, q (fullscreen QR), i (image mode), c/C/y
// (direct copy shortcuts). Runs before tab-specific dispatch so it
// works in both stOrdered (Swap tab) and Track tab. Returns
// handled=true when the keystroke was consumed.
func (m Model) handleDepositKeys(msg tea.KeyMsg) (Model, tea.Cmd, bool) {
var activeAddr string
var activeAddr, activeID string
if (m.tab == tabSwap || m.tab == tabGhost) && m.state == stOrdered && m.trade != nil {
activeAddr = m.trade.DepositAddress
activeID = tradeDisplayID(m.trade)
} else if m.tab == tabTrack && m.trackTrade != nil && !isTerminal(m.trackTrade.Status) {
activeAddr = m.trackTrade.DepositAddress
activeID = tradeDisplayID(m.trackTrade)
}
if activeAddr == "" {
if activeAddr == "" && activeID == "" {
return m, nil, false
}
switch msg.String() {
case "y":
if activeID == "" {
return m, nil, false
}
m.copyText(activeID)
m.copyToast = "📋 trade ID copied to clipboard"
return m, tea.Tick(2*time.Second, func(_ time.Time) tea.Msg { return clearToastMsg{} }), true
case "q":
if activeAddr == "" {
return m, nil, false
}
m.qrFullScreen = !m.qrFullScreen
return m, nil, true
case "i":
if activeAddr == "" {
return m, nil, false
}
m.qrImageMode = !m.qrImageMode
return m, nil, true
case "up", "k":
if activeAddr == "" {
return m, nil, false
}
if m.depositFocus > 0 {
m.depositFocus--
}
return m, nil, true
case "down", "j":
if activeAddr == "" {
return m, nil, false
}
if m.depositFocus < 1 {
m.depositFocus++
}
return m, nil, true
case "enter":
if activeAddr == "" {
return m, nil, false
}
var label string
if m.depositFocus == 0 {
label = "📋 address copied to clipboard"
Expand All @@ -517,10 +541,16 @@ func (m Model) handleDepositKeys(msg tea.KeyMsg) (Model, tea.Cmd, bool) {
m.copyToast = label
return m, tea.Tick(2*time.Second, func(_ time.Time) tea.Msg { return clearToastMsg{} }), true
case "c":
if activeAddr == "" {
return m, nil, false
}
m.copyText(activeAddr)
m.copyToast = "📋 address copied to clipboard"
return m, tea.Tick(2*time.Second, func(_ time.Time) tea.Msg { return clearToastMsg{} }), true
case "C":
if activeAddr == "" {
return m, nil, false
}
m.copyText(qrBrowserURL(activeAddr))
m.copyToast = "📋 QR URL copied to clipboard"
return m, tea.Tick(2*time.Second, func(_ time.Time) tea.Msg { return clearToastMsg{} }), true
Expand Down Expand Up @@ -829,6 +859,16 @@ func isTerminal(status string) bool {
return false
}

func tradeDisplayID(t *api.Trade) string {
if t == nil {
return ""
}
if t.ID != "" {
return t.ID
}
return t.TradeID
}

// fmtAmt renders a crypto amount with magnitude-aware precision so the
// route-card line stays on one row. Float64 round-trip precision (the
// previous `-1` flag) emitted 17 digits for values like 0.03507851359904113,
Expand Down Expand Up @@ -1120,7 +1160,7 @@ func (m Model) renderOrdered() string {
addrLine := osc8(depositURI, styleOk.Render(t.DepositAddress))
addrCaret, qrCaret := caretFor(m.depositFocus, 0), caretFor(m.depositFocus, 1)
left := []string{
styleAccent.Render("Order ") + t.ID,
styleAccent.Render("Order ") + tradeDisplayID(t) + styleDim.Render(" (y copy trade ID)"),
styleAccent.Render("Status: ") + styleOk.Render(strings.ToUpper(t.Status)),
"",
styleDim.Render("Send"),
Expand Down
68 changes: 59 additions & 9 deletions internal/tui/model_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package tui

import "testing"
import (
"bytes"
"strings"
"testing"

tea "github.com/charmbracelet/bubbletea"

"github.com/kyc-rip/cli/internal/api"
)

func TestSplitTickerNet(t *testing.T) {
cases := []struct {
in string
ticker, net string
in string
ticker, net string
}{
{"btc", "BTC", ""},
{"USDT-TRC20", "USDT", "TRC20"},
Expand All @@ -23,12 +31,12 @@ func TestSplitTickerNet(t *testing.T) {

func TestIsTerminal(t *testing.T) {
cases := map[string]bool{
"WAITING": false,
"finished": true,
"FINISHED": true,
"FAILED": true,
"EXPIRED": true,
"REFUNDED": true,
"WAITING": false,
"finished": true,
"FINISHED": true,
"FAILED": true,
"EXPIRED": true,
"REFUNDED": true,
"EXCHANGING": false,
}
for k, want := range cases {
Expand All @@ -37,3 +45,45 @@ func TestIsTerminal(t *testing.T) {
}
}
}

func TestOrderedTradeIDCopyShortcut(t *testing.T) {
var out bytes.Buffer
m := New(Config{ClipboardWriter: NewLockedWriter(&out)})
m.tab = tabSwap
m.state = stOrdered
m.trade = &api.Trade{
ID: "TRADE123",
Status: "WAITING",
DepositAddress: "deposit-address",
}

got, cmd, handled := m.handleDepositKeys(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'y'}})
if !handled {
t.Fatal("y key was not handled")
}
if cmd == nil {
t.Fatal("expected toast clear command")
}
if got.copyToast != "📋 trade ID copied to clipboard" {
t.Fatalf("copyToast = %q", got.copyToast)
}
if !strings.Contains(out.String(), osc52Clipboard("TRADE123")) {
t.Fatalf("clipboard output %q does not contain trade ID OSC52", out.String())
}
}

func TestOrderedViewDocumentsTradeIDCopyShortcut(t *testing.T) {
m := New(Config{})
m.tab = tabSwap
m.state = stOrdered
m.trade = &api.Trade{
ID: "TRADE123",
Status: "WAITING",
DepositAddress: "deposit-address",
}

view := m.renderOrdered()
if !strings.Contains(view, "y copy trade ID") {
t.Fatalf("ordered view does not document trade ID shortcut: %q", view)
}
}