-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
ScreenHome.go
243 lines (197 loc) · 6.38 KB
/
ScreenHome.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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
package Cli
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
"github.com/fsnotify/fsnotify"
"github.com/halleck45/ast-metrics/src/Analyzer"
pb "github.com/halleck45/ast-metrics/src/NodeType"
"github.com/muesli/termenv"
)
// General stuff for styling the view
var (
term = termenv.EnvColorProfile()
)
// ScreenHome is the home view
type ScreenHome struct {
isInteractive bool
files []*pb.File
projectAggregated Analyzer.ProjectAggregated
// program
tea *tea.Program
// modelChoices
modelChoices modelChoices
// watchers
FileWatcher *fsnotify.Watcher
currentModel tea.Model
}
// modelChoices is the model for the home view
type modelChoices struct {
files []*pb.File
projectAggregated Analyzer.ProjectAggregated
Choice int
// array of screens
screens []Screen
// Watcher
FileWatcher *fsnotify.Watcher
}
type DoRefreshModel struct {
files []*pb.File
projectAggregated Analyzer.ProjectAggregated
}
// NewScreenHome creates a new ScreenHome
func NewScreenHome(isInteractive bool, files []*pb.File, projectAggregated Analyzer.ProjectAggregated) *ScreenHome {
return &ScreenHome{
isInteractive: isInteractive,
files: files,
projectAggregated: projectAggregated,
}
}
// Render renders the home view and runs the Tea program
func (r *ScreenHome) Render() {
if r.tea != nil {
// If already running, just return
// send an update msg
//r.tea.Send(DoRefreshModel{files: r.files, projectAggregated: r.projectAggregated})
return
}
// Prepare list of accepted screens
m := modelChoices{files: r.files, projectAggregated: r.projectAggregated, FileWatcher: r.FileWatcher}
fillInScreens(&m)
r.currentModel = m
if !r.isInteractive {
// If not interactive
fmt.Println("No interactive mode detected.")
return
}
options := tea.WithAltScreen()
r.tea = tea.NewProgram(m, options)
if _, err := r.tea.Run(); err != nil {
fmt.Println("Error running program:", err)
r.tea.RestoreTerminal()
os.Exit(1)
}
}
func (r *ScreenHome) Reset(files []*pb.File, projectAggregated Analyzer.ProjectAggregated) {
r.files = files
r.projectAggregated = projectAggregated
// Update all screens
for _, s := range r.modelChoices.screens {
s.Reset(files, projectAggregated)
}
if r.tea == nil {
return
}
// Send update command to tea application
r.tea.Send(DoRefreshModel{files: files, projectAggregated: projectAggregated})
r.currentModel.Update(DoRefreshModel{files: files, projectAggregated: projectAggregated})
}
// Get Tea model
func (r ScreenHome) GetModel() modelChoices {
// Prepare list of accepted screens
m := modelChoices{files: r.files, projectAggregated: r.projectAggregated}
fillInScreens(&m)
return m
}
// fillInScreens fills in the screens array, and is used to avoid a circular dependency when creating the screens and coming back to the main screen
func fillInScreens(modelChoices *modelChoices) {
if len(modelChoices.screens) > 0 {
// we need to refresh screen only when --watch is set
// return
}
// Create the table screen
viewTableClass := NewScreenTableClass(true, modelChoices.files, modelChoices.projectAggregated)
// Create the table screen
summaryScreen := NewScreenSummary(true, modelChoices.files, modelChoices.projectAggregated)
// Create the Risk screen
viewRisks := NewScreenRisks(true, modelChoices.files, modelChoices.projectAggregated)
// Create the html report screen
viewHtmlReport := NewScreenHtmlReport(true, modelChoices.files, modelChoices.projectAggregated)
// Create the screen list
modelChoices.screens = []Screen{
&summaryScreen,
&viewHtmlReport,
&viewTableClass,
&viewRisks,
}
// Append one screen per programming language
for languageName, lang := range modelChoices.projectAggregated.ByProgrammingLanguage {
viewByProgrammingLanguage := ScreenByProgrammingLanguage{isInteractive: true}
viewByProgrammingLanguage.programmingLangageName = languageName
viewByProgrammingLanguage.programmingLangageAggregated = lang
viewByProgrammingLanguage.files = modelChoices.files
viewByProgrammingLanguage.projectAggregated = modelChoices.projectAggregated
modelChoices.screens = append(modelChoices.screens, &viewByProgrammingLanguage)
}
}
// Init initializes the Tea model
func (m modelChoices) Init() tea.Cmd {
return nil
}
// The main view, which just calls the appropriate sub-view
func (m modelChoices) View() string {
c := m.Choice
tpl := StyleTitle("AST Metrics").Render() +
"\n" + StyleSubTitle("AST Metrics is a language-agnostic static code analyzer. \n"+StyleUrl("https://github.com/Halleck45/ast-metrics").Render()).Render() +
fmt.Sprintf("\n\nAll %d files have been analyzed. What do you want to do next?\n\n", len(m.files))
choices := StyleHelp("Use arrows to navigate and esc to quit.").Render() + "\n\n"
for i, s := range m.screens {
label := s.GetScreenName()
if i == c {
choices += colorFg("[x] "+label, "212")
} else {
choices += fmt.Sprintf("[ ] %s", label)
}
choices += "\n"
}
tpl += StyleChoices(choices).Render()
tpl += "\n\nAST Metrics is an Open Source project. Contributions are welcome!\nDo not hesitate to open issue at " + StyleUrl("https://github.com/Halleck45/ast-metrics/issues").Render() + ". ❤️ Thanks!\n"
return StyleScreen(tpl).Render()
}
// Main update function.
func (m modelChoices) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Make sure these keys always quit
if msg, ok := msg.(tea.KeyMsg); ok {
k := msg.String()
if k == "q" || k == "esc" || k == "ctrl+c" {
// Check if we have a file watcher
if m.FileWatcher != nil {
m.FileWatcher.Close()
}
return m, tea.Quit
}
}
// issue when navigating back to the main screen
fillInScreens(&m)
switch msg := msg.(type) {
case DoRefreshModel:
// refresh the model, and the models of the sub screens
m.files = msg.files
m.projectAggregated = msg.projectAggregated
for _, s := range m.screens {
s.GetModel().Update(msg)
}
case tea.KeyMsg:
switch msg.String() {
case "j", "down":
m.Choice++
if m.Choice > (len(m.screens) - 1) {
m.Choice = len(m.screens) - 1
}
case "k", "up":
m.Choice--
if m.Choice < 0 {
m.Choice = 0
}
case "enter":
index := m.Choice
m.Choice = 0
return m.screens[index].GetModel(), tea.ClearScreen
}
}
return m, nil
}
// Color a string's foreground with the given value.
func colorFg(val, color string) string {
return termenv.String(val).Foreground(term.Color(color)).String()
}