package main import ( "fmt" "os" "path/filepath" "strings" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" "nymdrop/internal/reader" ) // submissionEntry is one row in the submissions list: enough to render the // row (label) and to load the full body on demand (path) without keeping // every decrypted body in memory at once. type submissionEntry struct { label string path string } // runGUI is the non-technical front-end for nymdrop-reader: a journalist // running this from a USB stick should never need a terminal. It drives the // same reader.Instance the CLI uses, through the same Hooks contract, so // there is exactly one implementation of the receive/decrypt/save logic. func runGUI(cfg reader.Config) { a := app.NewWithID("art.virebent.nymdrop-reader") w := a.NewWindow("NymDrop Reader") w.Resize(fyne.NewSize(900, 560)) statusLabel := widget.NewLabel("Starting...") statusLabel.Wrapping = fyne.TextWrapWord addrEntry := widget.NewEntry() addrEntry.Disable() addrEntry.SetPlaceHolder("Nym inbox address will appear here once connected") outDirLabel := widget.NewLabel("") var submissions []submissionEntry content := widget.NewMultiLineEntry() content.Wrapping = fyne.TextWrapWord content.Disable() list := widget.NewList( func() int { return len(submissions) }, func() fyne.CanvasObject { return widget.NewLabel("template") }, func(id widget.ListItemID, obj fyne.CanvasObject) { obj.(*widget.Label).SetText(submissions[id].label) }, ) list.OnSelected = func(id widget.ListItemID) { if id < 0 || id >= len(submissions) { return } data, err := os.ReadFile(submissions[id].path) if err != nil { content.SetText(fmt.Sprintf("error reading %s: %v", submissions[id].path, err)) return } content.SetText(string(data)) } hooks := reader.Hooks{ Log: func(format string, args ...any) { msg := fmt.Sprintf(format, args...) fyne.Do(func() { statusLabel.SetText(msg) }) }, SelfAddress: func(addr string) { fyne.Do(func() { addrEntry.SetText(addr) statusLabel.SetText("Connected -- waiting for submissions.") }) }, Submission: func(path, preview string) { ts := strings.TrimSuffix(filepath.Base(path), ".txt") oneLine := strings.ReplaceAll(preview, "\n", " ") if len(oneLine) > 80 { oneLine = oneLine[:80] + "..." } entry := submissionEntry{label: ts + " " + oneLine, path: path} fyne.Do(func() { // Newest first: a journalist checking the box wants the // latest tip on top, not scrolled to the bottom. submissions = append([]submissionEntry{entry}, submissions...) list.Refresh() statusLabel.SetText(fmt.Sprintf("%d submission(s) received.", len(submissions))) }) }, Attachment: func(path string, size int) { fyne.Do(func() { statusLabel.SetText(fmt.Sprintf("attachment saved: %s (%d bytes)", path, size)) }) }, Error: func(err error) { fyne.Do(func() { statusLabel.SetText("error: " + err.Error()) }) }, } in, err := reader.Start(cfg, hooks) if err != nil { statusLabel.SetText("failed to start: " + err.Error()) } else { outDirLabel.SetText("Submissions saved to: " + in.OutDir()) go in.ReceiveLoop() } top := container.NewVBox( widget.NewLabelWithStyle("NymDrop Reader", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), statusLabel, widget.NewForm(widget.NewFormItem("Nym inbox address", addrEntry)), outDirLabel, ) split := container.NewHSplit( container.NewBorder(widget.NewLabel("Submissions"), nil, nil, nil, list), container.NewBorder(widget.NewLabel("Content"), nil, nil, nil, content), ) split.Offset = 0.35 w.SetContent(container.NewBorder(top, nil, nil, nil, split)) w.SetCloseIntercept(func() { if in != nil { in.Close() } w.Close() }) w.ShowAndRun() }