diff --git a/cmd/ui.go b/cmd/ui.go index 0df7e8b..04ac849 100644 --- a/cmd/ui.go +++ b/cmd/ui.go @@ -40,6 +40,7 @@ type ui struct { imageBox *imageBox usageBox *box logBox *box + write chan []byte amb amiibo.Amiidump dec bool @@ -172,6 +173,7 @@ func newUi(invertImage bool) *ui { imageBox: image, usageBox: usage, logBox: logs, + write: make(chan []byte), } // TODO: prevent overwriting modals when they're active (like reading a new amiibo while the dump modal is open) @@ -179,8 +181,16 @@ func newUi(invertImage bool) *ui { load := newFilenameModal(s, boxOpts{title: "load dump", key: 'l', xPos: -1, yPos: -1, width: 30, height: 10, minHeight: 6, minWidth: 84}, logs.content, loadDump) // TODO: it would be cool to highlight the different data blocks in the hex dump (like ID, save data, ...) hex := newTextModal(s, boxOpts{title: "view dump as hex", key: 'h', xPos: -1, yPos: -1, width: 84, height: 36, typ: boxTypeCharacter, needAmiibo: true, scroll: true}, logs.content) + write := newOptionsModal( + s, + boxOpts{title: "write amiibo data to token", key: 'w', xPos: -1, yPos: -1, width: 80, height: 9, typ: boxTypeCharacter, needAmiibo: true}, + logs.content, + []mopts{{'f', "write full amiibo to token", 0}, {'u', "only write userdata to token (aka 'restore backup')", 1}}, + prepData, + u.write, + ) - u.elements = []element{info, image, usage, logs, actions, save, load, hex} + u.elements = []element{info, image, usage, logs, actions, save, load, write, hex} return u } @@ -222,6 +232,8 @@ func tui(conf *config) { u.setAmiibo(am, isAmiiboDecrypted(am, conf.retailKey)) showAmiiboInfo(u.amiibo(), u.isDecrypted(), u.logBox.content, u.infoBox.content, u.usageBox.content, u.imageBox, conf.amiiboApiBaseUrl) u.draw(false) + case data := <-u.write: + ptl.write(data[1:], data[0] == 1) case <-conf.quit: return } @@ -264,10 +276,6 @@ func tui(conf *config) { case e.Rune() == 'I' || e.Rune() == 'i': u.logBox.content <- encodeStringCell("Toggle image invert") u.imageBox.invertImage() - case e.Rune() == 'W' || e.Rune() == 'w': - if data := prepData(u.amiibo(), u.isDecrypted(), u.logBox.content); data != nil { - ptl.write(data, false) - } default: u.handleElementKey(e.Rune()) } diff --git a/cmd/ui_box_modal_options.go b/cmd/ui_box_modal_options.go new file mode 100644 index 0000000..4b826d3 --- /dev/null +++ b/cmd/ui_box_modal_options.go @@ -0,0 +1,85 @@ +package main + +import ( + "github.com/gdamore/tcell/v2" + "github.com/malc0mn/amiigo/amiibo" +) + +// mopts describes a single option for an options modal. +// The idea of using a key to select an option and not numbers before the options, is that the user +// is forced to pay proper attention as to what is going on. +type mopts struct { + key rune // The key to select the option, should be present in text: it will be rendered underlined. + text string // The text for the option. + value int // The value passed to the submit handler for the selected option. +} + +// optionsSubmitHandler defines a submithandler for an optionsModal, receiving the selected option +// value and an amiibo struct. +type optionsSubmitHandler func(value int, a amiibo.Amiidump, log chan<- []byte) []byte + +// optionsModal represents a modal that will request the user to select an option. It holds a +// channel to which the data of the submit handler will be sent before the modal is closed. +type optionsModal struct { + *modal + opts []mopts + submit optionsSubmitHandler + ret chan<- []byte +} + +// newOptionsModal creates a new optionsModal struct ready for use. +func newOptionsModal(s tcell.Screen, opts boxOpts, log chan<- []byte, mopts []mopts, submit optionsSubmitHandler, ret chan<- []byte) *optionsModal { + o := &optionsModal{opts: mopts, submit: submit, ret: ret} + o.modal = newModal(s, opts, o.handleInput, o.drawModalContent, nil, log) + + return o +} + +// handleInput will handle keyboard input for the optionsModal. +func (o *optionsModal) handleInput(e *tcell.EventKey) { + for _, opt := range o.opts { + if e.Rune() == opt.key { + o.ret <- o.submit(opt.value, o.a, o.log) + // Signal the modal is done. + o.end() + } + } +} + +// drawModalContent will handle displaying of the drawModalContent content. +func (o *optionsModal) drawModalContent(x, y int) { + x++ + y++ + start := x + prompt := "Please select an option by pressing the key of the underlined char:" + for _, char := range prompt { + o.drawChar(x, y, char, tcell.AttrNone) + x++ + } + y += 2 // Add blank line as well + start++ // Indent with one char for options + x = start // Back to start of line + + for _, opt := range o.opts { + optKey := false + o.drawChar(x, y, '•', tcell.AttrBold) + x += 2 + for _, char := range opt.text { + attr := tcell.AttrNone + if !optKey && char == opt.key { + attr = tcell.AttrUnderline | tcell.AttrBold + optKey = true + } + o.drawChar(x, y, char, attr) + x++ + } + x = start + y += 2 + } + o.s.Show() +} + +// drawChar draws a single char on the given position inside the modal. +func (o *optionsModal) drawChar(x, y int, c rune, attr tcell.AttrMask) { + o.s.SetContent(x, y, c, nil, tcell.StyleDefault.Background(backColour).Foreground(fontColour).Attributes(attr)) +} diff --git a/cmd/ui_functions.go b/cmd/ui_functions.go index 8859977..116a1f7 100644 --- a/cmd/ui_functions.go +++ b/cmd/ui_functions.go @@ -135,8 +135,8 @@ func saveDump(filename string, a amiibo.Amiidump, log chan<- []byte) bool { } // prepData gets the amiibo data in the correct format for writing to the NFC portal. -func prepData(a amiibo.Amiidump, dec bool, log chan<- []byte) []byte { - if dec { +func prepData(value int, a amiibo.Amiidump, log chan<- []byte) []byte { + if isAmiiboDecrypted(a, conf.retailKey) { log <- encodeStringCell("Refusing to write decrypted amiibo!") return nil } @@ -145,14 +145,19 @@ func prepData(a amiibo.Amiidump, dec bool, log chan<- []byte) []byte { return nil } + var data []byte + switch a.(type) { case *amiibo.Amiitool: - return amiibo.AmiitoolToAmiibo(a.(*amiibo.Amiitool)).Raw() + data = amiibo.AmiitoolToAmiibo(a.(*amiibo.Amiitool)).Raw() case *amiibo.Amiibo: - return a.Raw() + data = a.Raw() default: - panic(fmt.Sprintf("Unknown amiibo type!")) + log <- encodeStringCell("Cannot write: unknown amiibo type!") + return nil } + + return append([]byte{byte(value)}, data...) } // decrypt decrypts the given amiibo and returns a new amiibo.Amiidump instance.