From 530b484662a095f243d212fe10aef048ad0d2540 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 26 Nov 2025 21:46:17 -0300 Subject: [PATCH] beginnings of a qt gui. --- .gitignore | 1 + go.mod | 3 + go.sum | 19 ++++ view/.gitignore | 5 ++ view/Makefile | 13 +++ view/helpers.go | 48 ++++++++++ view/main.go | 229 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 318 insertions(+) create mode 100644 view/.gitignore create mode 100644 view/Makefile create mode 100644 view/helpers.go create mode 100644 view/main.go diff --git a/.gitignore b/.gitignore index 5f39b84..b983230 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ nak mnt nak.exe +qtbox diff --git a/go.mod b/go.mod index 1d52849..374c8f6 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,10 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-tty v0.0.7 github.com/mdp/qrterminal/v3 v3.2.1 + github.com/mitchellh/go-homedir v1.1.0 github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/stretchr/testify v1.10.0 + github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 golang.org/x/sync v0.18.0 @@ -50,6 +52,7 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/go-git/v5 v5.16.3 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect diff --git a/go.sum b/go.sum index 79ef597..f54b5ac 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e h1:XWcjeEtTFTOVA9Fs1w7n2XBftk5ib4oZrhzWk0B+3eA= +github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= @@ -131,6 +133,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -159,6 +163,8 @@ github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFe github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -187,7 +193,11 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -200,6 +210,8 @@ github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b h1:XeDLE6c9mzHpdv3W github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b/go.mod h1:7rwmCH0wC2fQvNEvPZ3sKXukhyCTyiaZ5VTZMQYpZKQ= github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o= +github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= @@ -229,6 +241,7 @@ go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= @@ -238,7 +251,9 @@ golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWI golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -251,10 +266,13 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -280,6 +298,7 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/view/.gitignore b/view/.gitignore new file mode 100644 index 0000000..4966360 --- /dev/null +++ b/view/.gitignore @@ -0,0 +1,5 @@ +dist +view +*.json +deploy +qtbox diff --git a/view/Makefile b/view/Makefile new file mode 100644 index 0000000..97a703c --- /dev/null +++ b/view/Makefile @@ -0,0 +1,13 @@ +dist: deploy/linux/nakv deploy/windows/nakv.exe + mkdir -p dist + cd deploy/linux && tar -czvf nakv_linux.tar.gz nakv + mv deploy/linux/nakv_linux.tar.gz dist/ + rm -f deploy/windows/nakv_windows.zip + cd deploy/windows && zip nakv_windows *.exe + mv deploy/windows/nakv_windows.zip dist/ + +deploy/linux/nakv: $(shell find . -name "*.go") + qtdeploy -ldflags="-s -w" -fast build desktop github.com/fiatjaf/nakv + +deploy/windows/nakv.exe: $(shell find . -name "*.go") + qtdeploy -ldflags="-s -w" -docker build windows_64_static diff --git a/view/helpers.go b/view/helpers.go new file mode 100644 index 0000000..7d27bbf --- /dev/null +++ b/view/helpers.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/keyer" + "fiatjaf.com/nostr/nip19" + "fiatjaf.com/nostr/nip46" +) + +func handleSecretKeyOrBunker(sec string) (nostr.SecretKey, nostr.Keyer, error) { + if strings.HasPrefix(sec, "bunker://") { + // it's a bunker + bunkerURL := sec + clientKey := nostr.Generate() + ctx := context.Background() + + bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {}) + if err != nil { + return nostr.SecretKey{}, nil, fmt.Errorf("failed to connect to %s: %w", bunkerURL, err) + } + + return nostr.SecretKey{}, keyer.NewBunkerSignerFromBunkerClient(bunker), err + } + + if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" { + return ski.(nostr.SecretKey), nil, nil + } + + sk, err := nostr.SecretKeyFromHex(sec) + if err != nil { + return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key: %w", err) + } + + return sk, keyer.NewPlainKeySigner(sk), nil +} + +func decodeTagValue(value string) string { + if strings.HasPrefix(value, "npub1") || strings.HasPrefix(value, "nevent1") || strings.HasPrefix(value, "note1") || strings.HasPrefix(value, "nprofile1") || strings.HasPrefix(value, "naddr1") { + if ptr, err := nip19.ToPointer(value); err == nil { + return ptr.AsTagReference() + } + } + return value +} diff --git a/view/main.go b/view/main.go new file mode 100644 index 0000000..37d2456 --- /dev/null +++ b/view/main.go @@ -0,0 +1,229 @@ +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() +}