-
Notifications
You must be signed in to change notification settings - Fork 212
/
modelUtils.go
200 lines (172 loc) Β· 5.27 KB
/
modelUtils.go
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package ui
import (
"bytes"
"fmt"
"maps"
"os"
"os/exec"
"text/template"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
log "github.com/charmbracelet/log"
"github.com/dlvhdr/gh-dash/v4/config"
"github.com/dlvhdr/gh-dash/v4/data"
"github.com/dlvhdr/gh-dash/v4/ui/common"
"github.com/dlvhdr/gh-dash/v4/ui/components/section"
"github.com/dlvhdr/gh-dash/v4/ui/constants"
"github.com/dlvhdr/gh-dash/v4/ui/context"
"github.com/dlvhdr/gh-dash/v4/ui/markdown"
)
func (m *Model) getCurrSection() section.Section {
sections := m.getCurrentViewSections()
if len(sections) == 0 || m.currSectionId >= len(sections) {
return nil
}
return sections[m.currSectionId]
}
func (m *Model) getCurrRowData() data.RowData {
section := m.getCurrSection()
if section == nil {
return nil
}
return section.GetCurrRow()
}
func (m *Model) getSectionAt(id int) section.Section {
sections := m.getCurrentViewSections()
if len(sections) <= id {
return nil
}
return sections[id]
}
func (m *Model) getPrevSectionId() int {
sectionsConfigs := m.ctx.GetViewSectionsConfig()
m.currSectionId = (m.currSectionId - 1) % len(sectionsConfigs)
if m.currSectionId < 0 {
m.currSectionId += len(sectionsConfigs)
}
return m.currSectionId
}
func (m *Model) getNextSectionId() int {
return (m.currSectionId + 1) % len(m.ctx.GetViewSectionsConfig())
}
type IssueCommandTemplateInput struct {
RepoName string
RepoPath string
IssueNumber int
HeadRefName string
}
func (m *Model) executeKeybinding(key string) tea.Cmd {
currRowData := m.getCurrRowData()
switch m.ctx.View {
case config.IssuesView:
for _, keybinding := range m.ctx.Config.Keybindings.Issues {
if keybinding.Key != key {
continue
}
switch data := currRowData.(type) {
case *data.IssueData:
return m.runCustomIssueCommand(keybinding.Command, data)
}
}
case config.PRsView:
for _, keybinding := range m.ctx.Config.Keybindings.Prs {
if keybinding.Key != key || keybinding.Command == "" {
continue
}
log.Debug("executing keybind", "key", keybinding.Key, "command", keybinding.Command)
switch data := currRowData.(type) {
case *data.PullRequestData:
return m.runCustomPRCommand(keybinding.Command, data)
}
}
default:
// Not a valid case - ignore it
}
return nil
}
// runCustomCommand executes a user-defined command.
// commandTemplate is a template string that will be parsed with the input data.
// contextData is a map of key-value pairs of data specific to the context the command is being run in.
func (m *Model) runCustomCommand(commandTemplate string, contextData *map[string]any) tea.Cmd {
// A generic map is a pretty easy & flexible way to populate a template if there's no pressing need
// for structured data, existing structs, etc. Especially if holes in the data are expected.
// Common data shared across contexts could be set here.
input := map[string]any{}
// Merge data specific to the context the command is being run in onto any common data, overwriting duplicate keys.
maps.Copy(input, *contextData)
// Append in the local RepoPath only if it can be found
if repoPath, ok := common.GetRepoLocalPath(input["RepoName"].(string), m.ctx.Config.RepoPaths); ok {
input["RepoPath"] = repoPath
}
cmd, err := template.New("keybinding_command").Parse(commandTemplate)
if err != nil {
log.Fatal("Failed parse keybinding template", "error", err)
}
// Set the command to error out if required input (e.g. RepoPath) is missing
cmd = cmd.Option("missingkey=error")
var buff bytes.Buffer
err = cmd.Execute(&buff, input)
if err != nil {
return func() tea.Msg {
return constants.ErrMsg{Err: fmt.Errorf("failed to parsetemplate %s", commandTemplate)}
}
}
return m.executeCustomCommand(buff.String())
}
func (m *Model) runCustomPRCommand(commandTemplate string, prData *data.PullRequestData) tea.Cmd {
return m.runCustomCommand(commandTemplate,
&map[string]any{
"RepoName": prData.GetRepoNameWithOwner(),
"PrNumber": prData.Number,
"HeadRefName": prData.HeadRefName,
"BaseRefName": prData.BaseRefName,
})
}
func (m *Model) runCustomIssueCommand(commandTemplate string, issueData *data.IssueData) tea.Cmd {
return m.runCustomCommand(commandTemplate,
&map[string]any{
"RepoName": issueData.GetRepoNameWithOwner(),
"IssueNumber": issueData.Number,
},
)
}
func (m *Model) executeCustomCommand(cmd string) tea.Cmd {
shell := os.Getenv("SHELL")
if shell == "" {
shell = "sh"
}
c := exec.Command(shell, "-c", cmd)
return tea.ExecProcess(c, func(err error) tea.Msg {
if err != nil {
mdRenderer := markdown.GetMarkdownRenderer(m.ctx.ScreenWidth)
md, mdErr := mdRenderer.Render(fmt.Sprintf("While running: `%s`", cmd))
if mdErr != nil {
return constants.ErrMsg{Err: mdErr}
}
return constants.ErrMsg{Err: fmt.Errorf(
lipgloss.JoinVertical(lipgloss.Left,
fmt.Sprintf("Whoops, got an error: %s", err),
md,
),
)}
}
return nil
})
}
func (m *Model) notify(text string) tea.Cmd {
id := fmt.Sprint(time.Now().Unix())
startCmd := m.ctx.StartTask(
context.Task{
Id: id,
StartText: text,
FinishedText: text,
State: context.TaskStart,
})
finishCmd := func() tea.Msg {
return constants.TaskFinishedMsg{
TaskId: id,
}
}
return tea.Sequence(startCmd, finishCmd)
}