build tab working.

This commit is contained in:
fiatjaf
2026-01-09 00:32:50 -03:00
parent 4376eef1fc
commit f4c6c3abee
4 changed files with 312 additions and 65 deletions

369
build.go
View File

@@ -3,14 +3,29 @@ package main
import (
"fmt"
"slices"
"strconv"
"strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/schema"
qt "github.com/mappu/miqt/qt6"
"github.com/mappu/miqt/qt6/mainthread"
)
type buildVars struct {
kindsCombo *qt.QComboBox
tab *qt.QWidget
kindsList *qt.QListWidget
items []kindItem
selected *kindItem
// dynamic form elements
descLabel *qt.QLabel
formLayout *qt.QVBoxLayout
formWidget *qt.QWidget
tagSections []*tagSection
contentEdit *qt.QTextEdit
contentLabel *qt.QLabel
}
type kindItem struct {
@@ -18,88 +33,320 @@ type kindItem struct {
schema.KindSchema
}
type tagSection struct {
isMultiple bool
isBoolean bool
isRequired bool
sectionBox *qt.QHBoxLayout
label *qt.QLabel
rowsBox *qt.QVBoxLayout
rows []*qt.QHBoxLayout
edits [][]*qt.QLineEdit
}
var build = &buildVars{}
func setupBuildTab() *qt.QWidget {
tab := qt.NewQWidget(window.QWidget)
build.tab = qt.NewQWidget(window.QWidget)
layout := qt.NewQVBoxLayout2()
tab.SetLayout(layout.QLayout)
build.tab.SetLayout(layout.QLayout)
// label
label := qt.NewQLabel2()
label.SetText("select a kind to build an event:")
layout.AddWidget(label.QWidget)
// dropdown
build.kindsCombo = qt.NewQComboBox(tab)
layout.AddWidget(build.kindsCombo.QWidget)
// search/filter input
searchInput := qt.NewQLineEdit(build.tab)
searchInput.SetPlaceholderText("filter kinds...")
layout.AddWidget(searchInput.QWidget)
// list widget for kind selection
build.kindsList = qt.NewQListWidget(build.tab)
build.kindsList.SetMaximumHeight(100)
layout.AddWidget(build.kindsList.QWidget)
// filter list items as user types
searchInput.OnTextChanged(func(text string) {
filterText := strings.ToLower(text)
for i := 0; i < build.kindsList.Count(); i++ {
item := build.kindsList.Item(i)
itemText := strings.ToLower(item.Text())
// TODO: instead of strings.Contains() use levenshtein distance fuzz matching
build.kindsList.SetRowHidden(i, !strings.Contains(itemText, filterText))
}
})
// description label
build.descLabel = qt.NewQLabel2()
build.descLabel.SetWordWrap(true)
layout.AddWidget(build.descLabel.QWidget)
// form container for dynamic tag inputs
build.formWidget = qt.NewQWidget(build.tab)
build.formLayout = qt.NewQVBoxLayout2()
build.formWidget.SetLayout(build.formLayout.QLayout)
layout.AddWidget(build.formWidget)
// content input
build.contentLabel = qt.NewQLabel2()
build.contentLabel.SetText("content:")
build.contentLabel.SetVisible(false)
layout.AddWidget(build.contentLabel.QWidget)
build.contentEdit = qt.NewQTextEdit(build.tab)
build.contentEdit.SetVisible(false)
build.contentEdit.SetMaximumHeight(40)
layout.AddWidget(build.contentEdit.QWidget)
// button to continue to event tab
buttonHBox := qt.NewQHBoxLayout2()
continueButton := qt.NewQPushButton5("event "+ARROW, build.tab)
continueButton.SetVisible(false)
buttonHBox.AddWidget(continueButton.QWidget)
buttonHBox.AddStretch()
layout.AddLayout(buttonHBox.QLayout)
continueButton.OnClicked(func() {
eventBuilt := build.buildEvent()
// TODO: check for missing tags in event
missing := []string{}
if len(missing) > 0 {
setStatus(tabs.build, "missing required tags: %s", strings.Join(missing, ", "))
return
}
// TODO: check each tag to see if it has all the necessary items and they match each of the types
// ('constrained', 'url', 'relay', 'id', 'addr', 'pubkey', 'timestamp', 'kind', 'giturl'. 'json', 'hex')
// ...
event.populate(eventBuilt)
tabWidget.SetCurrentIndex(tabs.event)
})
// spacer at bottom
layout.AddStretch()
// load kinds
loadKinds()
go func() {
sch, err := schema.FetchSchemaFromURL("https://raw.githubusercontent.com/nostr-protocol/registry-of-kinds/refs/heads/master/schema.yaml")
if err != nil {
fmt.Println("error loading schema:", err)
mainthread.Wait(func() {
setStatus(tabs.build, "error loading schema: %s", err)
})
return
}
return tab
build.items = make([]kindItem, 0, len(sch.Kinds))
for k, v := range sch.Kinds {
num := -1
fmt.Sscanf(k, "%d", &num)
if v.InUse {
build.items = append(build.items, kindItem{kind: uint16(num), KindSchema: v})
}
}
slices.SortFunc(build.items, func(a, b kindItem) int { return int(a.kind) - int(b.kind) })
mainthread.Wait(func() {
// populate list widget with items
for _, item := range build.items {
build.kindsList.AddItem(fmt.Sprintf("%d - %s", item.kind, item.Description))
}
// handle item selection
build.kindsList.OnItemClicked(func(li *qt.QListWidgetItem) {
row := build.kindsList.Row(li)
if row >= 0 && row < len(build.items) {
kindItem := build.items[row]
build.selected = &kindItem
item := kindItem.KindSchema
build.clearForm()
// show description
build.descLabel.SetText(item.Description)
// generate tag inputs based on schema
for _, tagSpec := range item.Tags {
isMultiple := slices.Contains(item.Multiple, tagSpec.Name)
if tagSpec.Prefix != "" {
isMultiple = isMultiple || slices.Contains(item.Multiple, tagSpec.Prefix)
}
build.addTag(&tagSpec, slices.Contains(item.Required, tagSpec.Name), isMultiple)
}
// show content input if the kind has content
hasContent := item.Content.Type != "empty"
build.contentLabel.SetVisible(hasContent)
build.contentEdit.SetVisible(hasContent)
if hasContent {
build.contentLabel.SetText(fmt.Sprintf("content (%s):", item.Content.Type))
}
continueButton.SetVisible(true)
}
})
})
}()
return build.tab
}
func loadKinds() {
sch, err := schema.FetchSchemaFromURL(schema.DefaultSchemaURL)
if err != nil {
fmt.Println("error loading schema:", err)
setStatus(tabs.build, "error loading schema: %s", err)
return
func (b *buildVars) addTag(tagSpec *schema.TagSpec, required bool, isMultiple bool) {
ts := &tagSection{
isMultiple: isMultiple,
isBoolean: tagSpec.Next == nil,
isRequired: required,
}
items := make([]kindItem, 0, len(sch.Kinds))
for k, v := range sch.Kinds {
num := -1
fmt.Sscanf(k, "%d", &num)
items = append(items, kindItem{kind: uint16(num), KindSchema: v})
ts.sectionBox = qt.NewQHBoxLayout2()
b.formLayout.AddLayout(ts.sectionBox.QLayout)
// label for the tag
labelText := tagSpec.Name
if required {
labelText += " (required)"
}
slices.SortFunc(items, func(a, b kindItem) int { return int(a.kind - b.kind) })
model := qt.NewQStandardItemModel2(len(items)+2, 2)
inUseCategory := qt.NewQStandardItem2("common")
inUseCategory.SetFlags(inUseCategory.Flags() ^ qt.ItemIsSelectable) // remove selectable flag
inUseCategory.SetData(qt.NewQVariant(), int(qt.FontRole)) // bold font
model.AppendRowWithItem(inUseCategory)
ts.label = qt.NewQLabel2()
ts.label.SetText(labelText + ":")
ts.label.SetMinimumWidth(80)
ts.sectionBox.AddWidget(ts.label.QWidget)
notInUseCategory := qt.NewQStandardItem2("weird")
notInUseCategory.SetFlags(inUseCategory.Flags() ^ qt.ItemIsSelectable) // remove selectable flag
notInUseCategory.SetData(qt.NewQVariant(), int(qt.FontRole)) // bold font
model.AppendRowWithItem(notInUseCategory)
b.tagSections = append(b.tagSections, ts)
for _, item := range items {
qitem := []*qt.QStandardItem{
qt.NewQStandardItem2(strconv.Itoa(int(item.kind))),
qt.NewQStandardItem2(item.Description),
// input for the tag value
if tagSpec.Next != nil {
vbox := qt.NewQVBoxLayout2()
ts.rowsBox = vbox
ts.sectionBox.AddLayout(vbox.Layout())
build.addTagRow(0, ts, tagSpec)
}
}
func (b *buildVars) addTagRow(i int, ts *tagSection, tagSpec *schema.TagSpec) {
hbox := qt.NewQHBoxLayout2()
ts.rowsBox.AddLayout(hbox.QLayout)
ts.rows = append(ts.rows, hbox)
ts.edits = append(ts.edits, make([]*qt.QLineEdit, 0, 3))
for curr := tagSpec.Next; curr != nil; curr = curr.Next {
// input for the tag value
edit := qt.NewQLineEdit(b.tab)
switch curr.Type {
case "constrained":
edit.SetPlaceholderText("\"" + strings.Join(curr.Either, "\" or \"") + "\"")
default:
edit.SetPlaceholderText(curr.Type)
}
if item.InUse {
inUseCategory.AppendRow(qitem)
} else {
notInUseCategory.AppendRow(qitem)
hbox.AddWidget(edit.QWidget)
ts.edits[i] = append(ts.edits[i], edit)
edit.OnTextChanged(func(text string) {
if !ts.isMultiple {
return
}
if strings.TrimSpace(text) != "" {
// if this is the last row and it has content and this tag accepts multiple values and add a new row
if i == len(ts.rowsBox.Children())-1 {
b.addTagRow(i+1, ts, tagSpec)
}
} else {
// now if this is the second to last row and the last row and it is fully empty we remove the last
if len(ts.rows) >= 2 && i == len(ts.rows)-2 {
shouldDelete := true
// if all inputs are empty in the last two rows we will delete
deletecheck:
for r := len(ts.rows) - 2; r < len(ts.edits); r++ {
for _, edit := range ts.edits[r] {
if strings.TrimSpace(edit.Text()) != "" {
shouldDelete = false
break deletecheck
}
}
}
if shouldDelete {
last := ts.rows[len(ts.rows)-1]
for _, edit := range ts.edits[len(ts.rows)-1] {
last.RemoveWidget(edit.QWidget)
edit.DeleteLater()
}
ts.edits = ts.edits[0 : len(ts.edits)-1]
ts.rowsBox.RemoveItem(last.QLayoutItem)
last.DeleteLater()
ts.rows = ts.rows[0 : len(ts.rows)-1]
}
}
}
})
}
}
func (b *buildVars) buildEvent() nostr.Event {
base := nostr.Event{
CreatedAt: nostr.Now(),
Kind: nostr.Kind(b.selected.kind),
Tags: make(nostr.Tags, 0, 3),
Content: b.contentEdit.ToPlainText(),
}
// of all the hboxes
for _, ts := range b.tagSections {
// if the second element of an hbox is a vbox that means it is a top-level hbox
// get tag name from the hbox
tagName := strings.TrimSuffix(
strings.SplitN(ts.label.Text(), " ", 2)[0],
":",
)
for e, edits := range ts.edits {
tag := make(nostr.Tag, 1, 4)
tag[0] = tagName
hasValue := false
for _, edit := range edits {
value := strings.TrimSpace(edit.Text())
tag = append(tag, value)
if value != "" {
hasValue = true
}
}
if ts.isBoolean || (ts.isRequired && e == 0) || hasValue {
base.Tags = append(base.Tags, tag)
}
}
}
proxy := qt.NewQSortFilterProxyModel()
proxy.SetSourceModel(model.QAbstractItemModel)
proxy.SetFilterCaseSensitivity(qt.CaseInsensitive)
build.kindsCombo.SetModel(proxy.QAbstractItemModel)
build.kindsCombo.SetEditable(true)
tableView := qt.NewQAbstractItemView2()
tableView.SetSelectionBehavior(qt.QAbstractItemView__SelectRows)
tableView.SetSelectionMode(qt.QAbstractItemView__SingleSelection)
build.kindsCombo.SetView(tableView)
build.kindsCombo.OnCurrentIndexChanged(func(index int) {
sourceIndex := proxy.MapToSource(proxy.Index(index, 0, qt.NewQModelIndex()))
actualIndex := sourceIndex.Row()
selectedItem := items[actualIndex]
fmt.Println("selected", selectedItem)
})
build.kindsCombo.OnEditTextChanged(func(text string) {
fmt.Println("########", text)
proxy.SetFilterFixedString(text)
})
return base
}
func (b *buildVars) clearForm() {
// clear all tag layouts and inputs
for _, ts := range b.tagSections {
ts.sectionBox.RemoveWidget(ts.label.QWidget)
ts.label.DeleteLater()
for r, row := range ts.rows {
for _, edit := range ts.edits[r] {
row.RemoveWidget(edit.QWidget)
edit.DeleteLater()
}
ts.rowsBox.RemoveItem(row.QLayoutItem)
row.DeleteLater()
}
ts.rowsBox.DeleteLater()
b.formLayout.RemoveItem(ts.sectionBox.QLayoutItem)
ts.sectionBox.DeleteLater()
}
b.tagSections = b.tagSections[:0]
// but keep the content just in case
// b.contentEdit.SetPlainText("")
}

View File

@@ -333,7 +333,7 @@ func (bunker *bunkerVars) startBunker() {
return true
}
authorized := slices.Contains(bunker.config.AuthorizedKeys, from) || slices.Contains([]string{}, secret) // TODO: authorized secrets
authorized := slices.Contains(bunker.config.AuthorizedKeys, from) || slices.Contains([]string{}, secret)
if authorized {
if harmless {
bunker.log("harmless request from %s", from.Hex())

View File

@@ -91,7 +91,7 @@ func setupEventTab() *qt.QWidget {
event.outputEdit = qt.NewQTextEdit(event.tab)
event.outputEdit.SetReadOnly(true)
outputHBox.AddWidget(event.outputEdit.QWidget)
copyButton := qt.NewQPushButton5(ARROW, event.tab)
copyButton := qt.NewQPushButton5("manual edit "+ARROW, event.tab)
outputHBox.AddWidget(copyButton.QWidget)
copyButton.OnClicked(func() {
tabWidget.SetCurrentIndex(tabs.paste)

4
req.go
View File

@@ -261,7 +261,7 @@ func setupReqTab() *qt.QWidget {
buttonsHBox := qt.NewQHBoxLayout2()
dlayout.AddLayout(buttonsHBox.QLayout)
editButton := qt.NewQPushButton5(ARROW+" paste", dialog.QWidget)
editButton := qt.NewQPushButton5("manual edit "+ARROW, dialog.QWidget)
editButton.OnClicked(func() {
paste.inputEdit.SetPlainText(string(pretty))
tabWidget.SetCurrentIndex(tabs.paste)
@@ -269,7 +269,7 @@ func setupReqTab() *qt.QWidget {
})
buttonsHBox.AddWidget(editButton.QWidget)
publishButton := qt.NewQPushButton5(ARROW+" event", dialog.QWidget)
publishButton := qt.NewQPushButton5("structured edit "+ARROW, dialog.QWidget)
publishButton.OnClicked(func() {
event.populate(evt)
tabWidget.SetCurrentIndex(tabs.event)