package main import ( "encoding/json" "os" "slices" "strings" "fiatjaf.com/nostr" "github.com/therecipe/qt/core" "github.com/therecipe/qt/widgets" ) var ( currentSec nostr.SecretKey currentKeyer nostr.Keyer tagRows [][]*widgets.QLineEdit updateEvent func() ) func main() { app := widgets.NewQApplication(len(os.Args), os.Args) window := widgets.NewQMainWindow(nil, 0) window.SetMinimumSize2(800, 600) window.SetWindowTitle("nakv") centralWidget := widgets.NewQWidget(nil, 0) window.SetCentralWidget(centralWidget) mainLayout := widgets.NewQVBoxLayout() centralWidget.SetLayout(mainLayout) // private key input secLabel := widgets.NewQLabel2("private key (hex or nsec):", nil, 0) mainLayout.AddWidget(secLabel, 0, 0) secEdit := widgets.NewQLineEdit(nil) mainLayout.AddWidget(secEdit, 0, 0) secEdit.ConnectTextChanged(func(text string) { if text == "" { currentSec = nostr.SecretKey{} currentKeyer = nil return } sk, bunker, err := handleSecretKeyOrBunker(text) if err != nil { // TODO: have a field somewhere at the bottom of the screen, below the tabs view, to display errors and other messages currentSec = nostr.SecretKey{} currentKeyer = nil return } currentSec = sk currentKeyer = bunker }) tabWidget := widgets.NewQTabWidget(nil) eventTab := widgets.NewQWidget(nil, 0) reqTab := widgets.NewQWidget(nil, 0) tabWidget.AddTab(eventTab, "event") tabWidget.AddTab(reqTab, "req") mainLayout.AddWidget(tabWidget, 0, 0) // set up event tab layout := widgets.NewQVBoxLayout() eventTab.SetLayout(layout) // kind input kindHBox := widgets.NewQHBoxLayout() layout.AddLayout(kindHBox, 0) kindLabel := widgets.NewQLabel2("kind:", nil, 0) kindHBox.AddWidget(kindLabel, 0, 0) kindSpin := widgets.NewQSpinBox(nil) // TODO: set default value to 1 kindSpin.SetMinimum(0) kindSpin.SetMaximum(1<<16 - 1) kindHBox.AddWidget(kindSpin, 0, 0) kindSpin.ConnectValueChanged(func(int) { updateEvent() }) kindNameLabel := widgets.NewQLabel2("", nil, 0) kindHBox.AddWidget(kindNameLabel, 0, 0) // content input contentLabel := widgets.NewQLabel2("content:", nil, 0) layout.AddWidget(contentLabel, 0, 0) contentEdit := widgets.NewQTextEdit(nil) layout.AddWidget(contentEdit, 0, 0) contentEdit.ConnectTextChanged(updateEvent) // created_at input createdAtLabel := widgets.NewQLabel2("created at:", nil, 0) layout.AddWidget(createdAtLabel, 0, 0) createdAtEdit := widgets.NewQDateTimeEdit(nil) createdAtEdit.SetDateTime(core.QDateTime_CurrentDateTime()) layout.AddWidget(createdAtEdit, 0, 0) createdAtEdit.ConnectDateTimeChanged(func(*core.QDateTime) { updateEvent() }) // tags input tagsLabel := widgets.NewQLabel2("tags:", nil, 0) layout.AddWidget(tagsLabel, 0, 0) tagsLayout := widgets.NewQVBoxLayout() tagRowHBoxes := make([]widgets.QLayout_ITF, 0, 2) layout.AddLayout(tagsLayout, 0) var addTagRow func() addTagRow = func() { hbox := widgets.NewQHBoxLayout() tagRowHBoxes = append(tagRowHBoxes, hbox) tagsLayout.AddLayout(hbox, 0) tagItems := []*widgets.QLineEdit{} y := len(tagRows) tagRows = append(tagRows, tagItems) var addItem func() addItem = func() { edit := widgets.NewQLineEdit(nil) hbox.AddWidget(edit, 0, 0) x := len(tagItems) tagItems = append(tagItems, edit) tagRows[y] = tagItems edit.ConnectTextChanged(func(text string) { if strings.TrimSpace(text) != "" { // when an item input has been filled check if we have to show more if y == len(tagRows)-1 { addTagRow() } if x == len(tagItems)-1 { addItem() } } else { // do this when an item input has been emptied: check if we need to remove an item from this row nItems := len(tagItems) if nItems >= 2 && strings.TrimSpace(tagItems[nItems-1].Text()) == "" && strings.TrimSpace(tagItems[nItems-2].Text()) == "" { // remove last item if the last 2 are empty hbox.Layout().RemoveWidget(tagItems[nItems-1]) tagItems[nItems-1].DeleteLater() tagItems = tagItems[0 : nItems-1] tagRows[y] = tagItems } // check if we need to remove rows nRows := len(tagRows) itemIsFilled := func(edit *widgets.QLineEdit) bool { return strings.TrimSpace(edit.Text()) != "" } if nRows >= 2 && !slices.ContainsFunc(tagRows[nRows-1], itemIsFilled) && !slices.ContainsFunc(tagRows[nRows-2], itemIsFilled) { // remove the last row if the last 2 are empty tagsLayout.RemoveItem(tagRowHBoxes[nRows-1]) for _, tagItem := range tagRows[nRows-1] { tagItem.DeleteLater() } tagRowHBoxes[nRows-1].QLayout_PTR().DeleteLater() tagRows = tagRows[0 : nRows-1] tagRowHBoxes = tagRowHBoxes[0 : nRows-1] } } updateEvent() }) } addItem() } // first addTagRow() // output JSON outputLabel := widgets.NewQLabel2("event:", nil, 0) layout.AddWidget(outputLabel, 0, 0) outputEdit := widgets.NewQTextEdit(nil) outputEdit.SetReadOnly(true) layout.AddWidget(outputEdit, 0, 0) // function to update the display updateEvent = func() { kind := nostr.Kind(kindSpin.Value()) kindName := kind.Name() if kindName != "unknown" { kindNameLabel.SetText(kindName) } else { kindNameLabel.SetText("") } tags := make(nostr.Tags, 0, len(tagRows)) for y, tagItems := range tagRows { if y == len(tagRows)-1 && strings.TrimSpace(tagItems[0].Text()) == "" { continue } tag := make(nostr.Tag, 0, len(tagItems)) for x, edit := range tagItems { text := strings.TrimSpace(edit.Text()) if x == len(tagItems)-1 && text == "" { continue } text = decodeTagValue(text) tag = append(tag, text) } if len(tag) > 0 { tags = append(tags, tag) } } event := nostr.Event{ Kind: kind, Content: contentEdit.ToPlainText(), CreatedAt: nostr.Timestamp(createdAtEdit.DateTime().ToMSecsSinceEpoch() / 1000), Tags: tags, } if currentKeyer != nil { // TODO: // call debounced function that calls: // currentKeyer.SignEvent(event) // then the json.Marshal / outputEdit.SetPlainText } jsonBytes, _ := json.MarshalIndent(event, "", " ") outputEdit.SetPlainText(string(jsonBytes)) } // initial render updateEvent() window.Show() app.Exec() }