-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
336 lines (280 loc) · 9.23 KB
/
main.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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/nuts-foundation/data-viewer/analyzers"
networkAPI "github.com/nuts-foundation/nuts-node/network/api/v1"
vdrAPI "github.com/nuts-foundation/nuts-node/vdr/api/v1"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
type appEvent string
const (
StartEvent appEvent = "start"
)
var showHelp bool = false
var showDebug bool = false
var hcursor int = 0
var vcursor int = 0
var lastPressed string
func main() {
if len(os.Args) >= 3 && os.Args[1] == "analyze" {
nodeAddress := os.Getenv("NUTS_NODE_ADDRESS")
if len(nodeAddress) == 0 {
log.Panic("NUTS_NODE_ADDRESS not set")
}
vdrClient, err := vdrAPI.NewClient(nodeAddress)
if err != nil {
log.Panic(err)
}
networkClient, err := networkAPI.NewClient(nodeAddress)
if err != nil {
log.Panic(err)
}
switch os.Args[2] {
case "did-graph":
if len(os.Args) < 4 {
log.Panic("analyze did-graph requires a DID as argument")
}
output, err := analyzers.DIDDocumentGraphAnalyzer{
VDR: vdrClient,
Network: networkClient,
}.Analyze(context.Background(), os.Args[3:])
if err != nil {
log.Panic(err)
}
fmt.Println(output)
os.Exit(0)
}
}
// Setup termui which provides primitives for terminal-based UI applications
if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
// Upon returning from main perform teardown operations for termui
defer ui.Close()
// Create channels for events from the UI as well as internal app events
uiEvents := ui.PollEvents()
appEvents := make(chan appEvent, 10)
// Put a start event in the app events channel
appEvents <- StartEvent
// Handle events as they occur
for {
// Wait for an event to occur
select {
// Process UI events (keyboard/mouse input, etc.)
case event := <-uiEvents:
log.Printf("got ui event: %v", event)
switch event.Type {
case ui.KeyboardEvent:
pressed := event.ID
keyboardEventHandler(pressed)
case ui.MouseEvent:
position := event.Payload.(ui.Mouse)
mouseEventHandler(position)
case ui.ResizeEvent:
dimensions := event.Payload.(ui.Resize)
resizeEventHandler(dimensions)
}
// Process app events (startup etc.)
case event := <-appEvents:
log.Printf("got app event: %v", event)
}
// Render the application content
render()
}
}
func resizeEventHandler(dimensions ui.Resize) {}
func mouseEventHandler(position ui.Mouse) {}
var keyboardReadLineBuffer string
func keyboardEventHandler(pressed string) {
if pressed == "#" {
keyboardReadLineBuffer = pressed
} else if keyboardReadLineBuffer != "" && strings.Contains("0123456789", pressed) {
keyboardReadLineBuffer += pressed
} else if keyboardReadLineBuffer != "" && pressed == "<Enter>" && !strings.HasSuffix(keyboardReadLineBuffer, "\n") {
keyboardReadLineBuffer += "\n"
} else {
keyboardReadLineBuffer = ""
if pressed == "q" || pressed == "Q" {
ui.Close()
os.Exit(0)
} else if pressed == "?" || pressed == "<F1>" {
showHelp = !showHelp
} else if pressed == "ß" /* Option-D */ {
showDebug = !showDebug
} else if pressed == "<Left>" {
hcursor--
} else if pressed == "<Right>" {
hcursor++
} else if pressed == "<Up>" {
vcursor--
} else if pressed == "<Down>" {
vcursor++
}
}
lastPressed = pressed
}
func render() {
// Clear any existing content on the terminal
ui.Clear()
renderDAG()
// Optionally show the help screen on top of the app
if showHelp {
// Determine the size of the terminal in characters
width, height := ui.TerminalDimensions()
p := widgets.NewParagraph()
p.Title = "| Help |"
p.Text = "q | Q - quit\n" +
"? | <F1> - show/hide help\n" +
"\n" +
"#𝑁<Enter> - select transaction number 𝑁 \n" +
"\n" +
"y - copy raw transaction to clipboard (OSC52)" +
"Home | g - go to transaction 0.0\n" // TODO: Implement this
p.SetRect(0, 0, width-1, height-1)
ui.Render(p)
}
if showDebug {
// Determine the size of the terminal in characters
width, height := ui.TerminalDimensions()
p := widgets.NewParagraph()
p.Title = "| Debug |"
p.Text = "test keyboard: " + lastPressed + "\n" +
"test readline: " + keyboardReadLineBuffer
p.SetRect(0, 0, width-1, height-1)
ui.Render(p)
}
}
type transactionMap map[int][]string
var transactions transactionMap
var dagLamportClock int
var dagSubIndex int
var dagMaxLamportClock int = 9999 // TODO: This must not be hard coded
func renderDAG() {
// Handle the user manually entering a transaction number
if strings.HasSuffix(keyboardReadLineBuffer, "\n") {
s := strings.TrimLeft(strings.TrimRight(keyboardReadLineBuffer, "\n"), "#")
if n, err := strconv.ParseInt(s, 10, 32); err == nil {
dagLamportClock = int(n)
dagSubIndex = 0
} else {
log.Panicf("strconv error: %v", err)
}
keyboardReadLineBuffer = ""
}
// Handle the user browsing the DAG
if hcursor != 0 {
// Handle the user navigating left
if hcursor < 0 {
// Decrement the sub index within a particular lamport clock if possible
if dagSubIndex > 0 {
dagSubIndex--
// Otherwise decrement the lamport clock if possible, resetting the sub index
} else if dagLamportClock > 0 {
dagLamportClock--
// Reset the sub index to select the "rightmost" transaction within the
// new lamport clock
// TODO: FIX BUG HERE: dagSubIndex = len(transactions[dagLamportClock])-1
dagSubIndex = 0 // TODO: Temporary hack for bug ^^
}
// Handle the user navigating right
} else {
// Increment the sub index within a particular lamport clock if possible
if dagSubIndex+1 < len(transactions[dagLamportClock]) {
dagSubIndex++
// Otherwise increment the lamport clock if possible, resetting the sub index
} else if dagLamportClock < dagMaxLamportClock {
dagLamportClock++
// Reset the sub index to select the "leftmost" transaction within the
// new lamport clock
dagSubIndex = 0
}
}
// Reset the hcursor to 0 so that future navigation can be handled properly
hcursor = 0
}
// If needed load the transactions for the desired lamport clock
if _, ok := transactions[dagLamportClock]; !ok {
// Load the transactions for this lamport clock into the transactions map
transactions[dagLamportClock] = fetchTransactionsInRange(dagLamportClock, dagLamportClock+1)
}
// Support OSC52 clipboard copy of raw transaction data
if lastPressed == "y" {
print("\033]52;c;" + base64.StdEncoding.EncodeToString([]byte(transactions[dagLamportClock][dagSubIndex])) + "\a")
lastPressed = "" // TODO: This should not be necessary and is a bit hacky
}
// Create a new paragraph UI widget, which can render arbitrary text
p := widgets.NewParagraph()
// Show the transaction # as a decimal, so that the lamport clock and sub index are visible
if len(transactions[dagLamportClock]) > 1 {
p.Title = fmt.Sprintf("| Transaction %d.%d |", dagLamportClock, dagSubIndex)
// Unless there's only one, in which case just show the lamport clock
} else {
p.Title = fmt.Sprintf("| Transaction %d |", dagLamportClock)
}
// Split the transaction on dots (".") in which the first part is the base64 encoded JSON data
transactionParts := strings.Split(transactions[dagLamportClock][dagSubIndex], ".")
// If the transaction split was successful then perform base64 and JSON decoding
if transactionParts != nil {
// Decode the raw base64 data of the transaction
if rawJSON, err := base64.RawStdEncoding.DecodeString(transactionParts[0]); err == nil {
// Nicely format and indent the JSON
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, rawJSON, "", " "); err == nil {
p.Text = prettyJSON.String()
} else {
p.Text = err.Error()
}
} else {
// Render any decode errors
p.Text = err.Error()
}
} else {
p.Text = "error: string split failed"
}
// Determine the size of the terminal in characters
width, height := ui.TerminalDimensions()
// Use all available terminal space for the render
p.SetRect(0, 0, width, height)
// Print the UI to the terminal
ui.Render(p)
}
// fetchTransactionsInRange returns the transactions where start <= lamport clock < end
func fetchTransactionsInRange(start int, end int) []string {
// Build the URL and place the start/end of the lamport clock range in the query string
url := fmt.Sprintf("http://127.0.0.1:1323/internal/network/v1/transaction?start=%d&end=%d", start, end)
// Call the API endpoint
response, err := http.Get(url)
// If there is a response with a body ensure it is deallocated later
if response != nil && response.Body != nil {
defer response.Body.Close()
}
// If an error occurred then report an error condition
if err != nil {
log.Panicf("HTTP request failed: %v", err)
}
// Read the response body contents, risking memory allocation issues
body, err := io.ReadAll(response.Body)
// Handle any errors that occurred in the response body reading
if err != nil {
log.Panicf("failed to read response body: %v", err)
}
// Parse the JSON from the body
var transactions []string
json.Unmarshal(body, &transactions)
// Return the transactions within the matching lambert clock range
return transactions
}
func init() {
transactions = make(transactionMap)
}