package ui import ( "fmt" "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "golang.org/x/crypto/ssh" "gitea.apointless.space/bsncubed/haul/internal/sshx" "gitea.apointless.space/bsncubed/haul/internal/xfer" ) // The prompts in this file are all shaped the same way, because they are all // answers a background goroutine is blocked waiting for: the goroutine makes a // buffered channel, asks for the prompt to be shown on the UI thread, and reads // its answer. Every dismissal path — button, escape, window close — must send // exactly once, or the transfer goroutine hangs forever holding the connection. // sync.Once is what guarantees the "exactly". func confirmDialog(title, msg string, win fyne.Window, cb func(bool)) { dialog.ShowConfirm(title, msg, cb, win) } func newFormDialog(title, confirm string, items []*widget.FormItem, win fyne.Window, cb func(bool)) dialog.Dialog { d := dialog.NewForm(title, confirm, "Cancel", items, cb, win) d.Resize(fyne.NewSize(420, 0)) return d } // conflictAnswer is the user's decision about one pre-existing destination file. type conflictAnswer struct { choice xfer.Conflict all bool // apply this decision to every remaining conflict } // askConflict shows the overwrite prompt. It must be called on the UI thread. func askConflict(win fyne.Window, name string, reply chan<- conflictAnswer) { var once sync.Once send := func(a conflictAnswer) { once.Do(func() { reply <- a }) } all := widget.NewCheck("Apply to all remaining conflicts", nil) msg := widget.NewLabel(fmt.Sprintf("%s already exists at the destination.", name)) msg.Wrapping = fyne.TextWrapWord var d *dialog.CustomDialog button := func(label string, c xfer.Conflict) *widget.Button { return widget.NewButton(label, func() { send(conflictAnswer{choice: c, all: all.Checked}) d.Hide() }) } overwrite := button("Overwrite", xfer.Overwrite) overwrite.Importance = widget.HighImportance content := container.NewVBox( msg, all, container.NewHBox(layout.NewSpacer(), button("Skip", xfer.Skip), button("Keep both", xfer.Rename), overwrite, ), ) d = dialog.NewCustomWithoutButtons("File exists", content, win) // Dismissing without choosing is treated as "skip this file": the safe // reading of "I didn't answer" is "don't destroy anything". d.SetOnClosed(func() { send(conflictAnswer{choice: xfer.Skip}) }) d.Resize(fyne.NewSize(460, 0)) d.Show() } // askSecret prompts for a password or key passphrase. An empty result means the // user declined, which callers pass on to sshx as "skip this method". func askSecret(win fyne.Window, title, prompt string, reply chan<- []byte) { var once sync.Once send := func(b []byte) { once.Do(func() { reply <- b }) } entry := widget.NewPasswordEntry() label := widget.NewLabel(prompt) label.Wrapping = fyne.TextWrapWord var d *dialog.CustomDialog submit := func() { send([]byte(entry.Text)) d.Hide() } entry.OnSubmitted = func(string) { submit() } ok := widget.NewButton("OK", submit) ok.Importance = widget.HighImportance cancel := widget.NewButton("Cancel", func() { send(nil) d.Hide() }) content := container.NewVBox( label, entry, container.NewHBox(layout.NewSpacer(), cancel, ok), ) d = dialog.NewCustomWithoutButtons(title, content, win) d.SetOnClosed(func() { send(nil) }) d.Resize(fyne.NewSize(420, 0)) d.Show() win.Canvas().Focus(entry) } // askHostKey shows the unknown-host prompt, the GUI equivalent of ssh's // "authenticity of host ... can't be established". The fingerprint is shown in // the same SHA256 form ssh prints, so it can be compared by eye. func askHostKey(win fyne.Window, host string, key ssh.PublicKey, reply chan<- bool) { var once sync.Once send := func(b bool) { once.Do(func() { reply <- b }) } msg := widget.NewLabel(fmt.Sprintf( "The authenticity of host %s can't be established.\n\n%s key fingerprint:", host, key.Type())) msg.Wrapping = fyne.TextWrapWord fp := widget.NewLabelWithStyle(sshx.FingerprintSHA256(key), fyne.TextAlignCenter, fyne.TextStyle{Monospace: true}) warn := widget.NewLabel("Only continue if you recognise this fingerprint. It will be added to your known_hosts file.") warn.Wrapping = fyne.TextWrapWord var d *dialog.CustomDialog accept := widget.NewButton("Connect and record key", func() { send(true) d.Hide() }) accept.Importance = widget.HighImportance reject := widget.NewButton("Cancel", func() { send(false) d.Hide() }) content := container.NewVBox(msg, fp, warn, container.NewHBox(layout.NewSpacer(), reject, accept)) d = dialog.NewCustomWithoutButtons("Unknown host key", content, win) d.SetOnClosed(func() { send(false) }) d.Resize(fyne.NewSize(520, 0)) d.Show() }