1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
package export
import (
"errors"
"fmt"
"strings"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
// Option is a selectable export target.
type Option int
const (
OptionCSV Option = iota
OptionCancel
)
var optionLabels = []string{
"CSV stream rows",
"Cancel",
}
var optionValues = []Option{
OptionCSV,
OptionCancel,
}
// RequestMsg asks the parent model to perform an export.
type RequestMsg struct {
Option Option
}
// CompletedMsg reports a finished export with output path.
type CompletedMsg struct {
Path string
}
// FailedMsg reports an export error.
type FailedMsg struct {
Err error
}
// Model is the export modal state machine.
type Model struct {
visible bool
selected int
exporting bool
status string
}
// NewModel creates a closed export modal.
func NewModel() Model {
return Model{}
}
func (m Model) Visible() bool { return m.visible }
func (m Model) Open() Model {
m.visible = true
m.selected = 0
m.exporting = false
m.status = ""
return m
}
func (m Model) Close() Model {
m.visible = false
m.exporting = false
m.status = ""
return m
}
// Update handles modal key navigation and export completion messages.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
if !m.visible {
return m, nil
}
if m.exporting {
if msg.String() == "esc" {
return m.Close(), nil
}
return m, nil
}
switch msg.String() {
case "esc":
return m.Close(), nil
case "up", "k":
if m.selected > 0 {
m.selected--
}
return m, nil
case "down", "j":
if m.selected < len(optionValues)-1 {
m.selected++
}
return m, nil
case "enter":
option := optionValues[m.selected]
if option == OptionCancel {
return m.Close(), nil
}
m.exporting = true
m.status = fmt.Sprintf("Exporting %s...", optionLabels[m.selected])
return m, func() tea.Msg { return RequestMsg{Option: option} }
}
case CompletedMsg:
m.exporting = false
if msg.Path == "" {
msg.Path = "done"
}
m.status = "Exported: " + msg.Path
return m, nil
case FailedMsg:
m.exporting = false
if msg.Err == nil {
msg.Err = errors.New("unknown export failure")
}
m.status = "Export failed: " + msg.Err.Error()
return m, nil
}
return m, nil
}
// View renders a centered modal overlay.
func (m Model) View(width, height int) string {
if !m.visible {
return ""
}
if width <= 0 {
width = 80
}
if height <= 0 {
height = 24
}
modalWidth := 48
if width < modalWidth+4 {
modalWidth = width - 4
if modalWidth < 30 {
modalWidth = 30
}
}
lines := []string{"Export Stream CSV"}
for i, label := range optionLabels {
prefix := " "
if i == m.selected && !m.exporting {
prefix = "> "
}
lines = append(lines, prefix+label)
}
if m.status != "" {
lines = append(lines, "", m.status)
}
if !m.exporting {
lines = append(lines, "", "Enter confirm • Esc cancel")
}
box := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
Padding(1, 2).
Width(modalWidth).
Render(strings.Join(lines, "\n"))
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, box)
}
|