diff --git a/.gitignore b/.gitignore index b436743..642f911 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /server -/cli *.properties .DS_Store /vendor diff --git a/Makefile b/Makefile index ae65042..bab2288 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,21 @@ -.PHONY: up down run-server run-cli test +.PHONY: up down run test GOFILES=$(shell find . -name '*.go' -o -name 'go.*') STATICFILES=$(shell find . -name '*.js' -o -name '*.css' -o -name '*.html') -SQLFILES=$(shell find . -name '*.sql') ifneq (,$(wildcard ./local.properties)) include local.properties export endif -build: server cli +build: server -run-server: build +run: build ./server -run-cli: build - ./cli - server: $(GOFILES) $(STATICFILES) go build -o server ./cmd/serve -cli: $(GOFILES) $(SQLFILES) - go build -o cli ./cmd/cli - test: go test ./... -cover diff --git a/cli b/cli new file mode 100755 index 0000000..fced169 Binary files /dev/null and b/cli differ diff --git a/cmd/cli/events.go b/cmd/cli/events.go deleted file mode 100644 index 159d46c..0000000 --- a/cmd/cli/events.go +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - "git.yetaga.in/alazyreader/library/media" - "github.com/gdamore/tcell/v2" -) - -// error message -type EventError struct { - tcell.EventTime - err error -} - -func NewEventError(err error) *EventError { - e := &EventError{err: err} - e.SetEventNow() - return e -} - -// save change to book -type EventBookUpdate struct { - tcell.EventTime - book *media.Book -} - -func NewEventBookUpdate(b *media.Book) *EventBookUpdate { - e := &EventBookUpdate{book: b} - e.SetEventNow() - return e -} - -func (e *EventBookUpdate) Book() *media.Book { - return e.book -} - -// open new book in display -type EventLoadBook struct { - tcell.EventTime - ID int -} - -func NewEventLoadBook(id int) *EventLoadBook { - e := &EventLoadBook{ID: id} - e.SetEventNow() - return e -} - -// open new book in display -type EventEnterBook struct { - tcell.EventTime -} - -func NewEventEnterBook() *EventEnterBook { - e := &EventEnterBook{} - e.SetEventNow() - return e -} - -// switch back to menu control -type EventExitBook struct { - tcell.EventTime -} - -func NewEventExitBook() *EventExitBook { - e := &EventExitBook{} - e.SetEventNow() - return e -} - -// open import window -type EventOpenImport struct { - tcell.EventTime -} - -func NewEventOpenImport() *EventOpenImport { - e := &EventOpenImport{} - e.SetEventNow() - return e -} - -// attempt to import given filename.csv -type EventAttemptImport struct { - tcell.EventTime - filename string -} - -func NewEventAttemptImport(f string) *EventAttemptImport { - e := &EventAttemptImport{filename: f} - e.SetEventNow() - return e -} - -// close import window -type EventCloseImport struct { - tcell.EventTime -} - -func NewEventCloseImport() *EventCloseImport { - e := &EventCloseImport{} - e.SetEventNow() - return e -} - -// quit -type EventQuit struct { - tcell.EventTime -} - -func NewEventQuit() *EventQuit { - e := &EventQuit{} - e.SetEventNow() - return e -} diff --git a/cmd/cli/main.go b/cmd/cli/main.go deleted file mode 100644 index 60f8552..0000000 --- a/cmd/cli/main.go +++ /dev/null @@ -1,336 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "runtime/debug" - "strings" - "sync" - - "git.yetaga.in/alazyreader/library/config" - "git.yetaga.in/alazyreader/library/database" - "git.yetaga.in/alazyreader/library/importer" - "git.yetaga.in/alazyreader/library/media" - "git.yetaga.in/alazyreader/library/ui" - "github.com/gdamore/tcell/v2" - "github.com/kelseyhightower/envconfig" -) - -// State holds the UI state keys=>value map and manages access to the map with a mutex -type State struct { - m sync.Mutex - stateMap map[string]interface{} -} - -// key, present -func (s *State) Get(key string) interface{} { - s.m.Lock() - defer s.m.Unlock() - if s.stateMap == nil { - s.stateMap = make(map[string]interface{}) - } - k, ok := s.stateMap[key] - if !ok { - return nil - } - return k -} - -// key, value -func (s *State) Set(key string, value interface{}) { - s.m.Lock() - defer s.m.Unlock() - if s.stateMap == nil { - s.stateMap = make(map[string]interface{}) - } - s.stateMap[key] = value -} - -func max(a, b int) int { - if a > b { - return a - } - return b -} - -// UI states -const ( - IN_MENU = iota - IN_BOOK - IN_IMPORT -) - -func main() { - var c config.Config - err := envconfig.Process("library", &c) - if err != nil { - log.Fatalln(err) - } - - // create state - state := State{} - - // set up DB connection - if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" { - if c.DBPass != "" { // obscure password - c.DBPass = c.DBPass[0:max(3, len(c.DBPass))] + strings.Repeat("*", max(0, len(c.DBPass)-3)) - } - log.Fatalf("vars: %+v", c) - } - lib, err := database.NewMySQLConnection(c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName) - if err != nil { - log.Fatalln(err) - } - err = lib.PrepareDatabase(context.Background()) - if err != nil { - log.Fatalln(err) - } - _, _, err = lib.RunMigrations(context.Background()) - if err != nil { - log.Fatalln(err) - } - books, err := lib.GetAllBooks(context.Background()) - if err != nil { - log.Fatalln(err) - } - state.Set("library", books) - - screen, err := tcell.NewScreen() - if err != nil { - log.Fatalln(err) - } - err = screen.Init() - if err != nil { - log.Fatalln(err) - } - // cleanup our screen and log if we panic and crash out somewhere - defer func() { - if r := recover(); r != nil { - if screen != nil { - screen.Fini() - } - fmt.Println("fatal panic;", r) - if c.Debug { - fmt.Println("stacktrace: \n" + string(debug.Stack())) - } - return - } - }() - - // book list and options menu (left column) - l := ui.NewList(Titles(state.Get("library").([]media.Book)), 0) - menu := ui.NewBox( - "library", - []string{"˄˅ select", "⏎ edit", "(n)ew", "(i)mport", "(q)uit"}, - ui.Contents{{ - Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2}, - Container: l, - }}, - ui.StyleActive, - false, - ) - activeBookDetails := ui.NewBookDetails(&media.Book{}) - - // book display (right column) - activeBook := ui.NewBox( - "book", - []string{"˄˅ select", "⏎ edit", "(esc) close"}, - ui.Contents{{ - Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0}, - Container: activeBookDetails, - }}, - ui.StyleInactive, - false, - ) - - // parent container - container := ui.NewContainer( - ui.Contents{ - {Container: menu, Offsets: ui.Offsets{Percent: 1}}, - {Container: activeBook, Offsets: ui.Offsets{Percent: 2}}, - }, - ui.LayoutHorizontalPercent, - ) - - // import pop-up - wd, _ := os.Getwd() - fileSelector := ui.NewEditableTextLine(wd) - fileSelector.ResetCursor(false) - fileSelector.SetStyle(ui.StyleActive.Underline(true)) - popup := ui.NewBox( - "import csv file", - []string{"⏎ submit", "(esc)close"}, - ui.Contents{ - {Container: fileSelector, Offsets: ui.Offsets{Top: 2, Left: 2}}, - }, - ui.StyleActive, - false, - ) - popup.SetVisible(false) - - // error pop-up - errorMessage := ui.NewEditableTextLine("") - errorPopup := ui.NewBox( - "error", - []string{"⏎ close"}, - ui.Contents{ - {Container: errorMessage, Offsets: ui.Offsets{Top: 1, Left: 1}}, - }, - ui.StyleActive.Bold(true).Foreground(tcell.ColorRed), - false, - ) - errorPopup.SetVisible(false) - - // init - screen.Clear() - w, h := screen.Size() - container.SetSize(0, 0, h, w) - container.Draw(screen) - screen.Sync() - - // init UI state - state.Set("ui_state", IN_MENU) - screen.PostEvent(NewEventLoadBook(l.SelectedID())) - - // UI loop - for { - e := screen.PollEvent() - switch v := e.(type) { - case *tcell.EventError: - fmt.Fprintf(os.Stderr, "%v", v) - screen.Beep() - case *tcell.EventKey: // input handling - curr := state.Get("ui_state").(int) - if curr == IN_MENU { - if v.Key() == tcell.KeyUp && l.Selected() > 0 { - l.SetSelected(l.Selected() - 1) - screen.PostEvent(NewEventLoadBook(l.SelectedID())) - } - if v.Key() == tcell.KeyDown && l.Selected() < len(l.ListMembers())-1 { - l.SetSelected(l.Selected() + 1) - screen.PostEvent(NewEventLoadBook(l.SelectedID())) - } - if v.Key() == tcell.KeyEnter { - screen.PostEvent(NewEventEnterBook()) - } - if v.Rune() == 'i' { - screen.PostEvent(NewEventOpenImport()) - } - if v.Rune() == 'q' { - screen.PostEvent(NewEventQuit()) - } - } else if curr == IN_BOOK { - if v.Key() == tcell.KeyEsc { - screen.PostEvent(NewEventExitBook()) - } - } else if curr == IN_IMPORT { - if v.Key() == tcell.KeyEsc { - fileSelector.SetText(wd) - fileSelector.ResetCursor(false) - screen.PostEvent(NewEventCloseImport()) - } - if v.Key() == tcell.KeyBackspace || v.Key() == tcell.KeyBackspace2 { - fileSelector.DeleteAtCursor() - } else if v.Key() == tcell.KeyRight { - fileSelector.MoveCursor(1) - - } else if v.Key() == tcell.KeyLeft { - fileSelector.MoveCursor(-1) - } else if v.Key() == tcell.KeyEnter { - screen.PostEvent(NewEventAttemptImport(fileSelector.Text())) - } else if v.Rune() != 0 { - fileSelector.InsertAtCursor(v.Rune()) - } - } - case *tcell.EventResize: // screen redraw - w, h := screen.Size() - container.SetSize(0, 0, h, w) - case *EventBookUpdate: - // TK - case *EventEnterBook: - activeBook.SetStyle(ui.StyleActive) - menu.SetStyle(ui.StyleInactive) - state.Set("ui_state", IN_BOOK) - case *EventExitBook: - state.Set("ui_state", IN_MENU) - activeBook.SetStyle(ui.StyleInactive) - menu.SetStyle(ui.StyleActive) - case *EventLoadBook: - activeBookDetails.SetBook(GetBookByID(v.ID, books)) - case *EventOpenImport: - state.Set("ui_state", IN_IMPORT) - menu.SetStyle(ui.StyleInactive) - popup.SetVisible(true) - popup.SetSize(6, 3, 5, 80) - case *EventAttemptImport: - // this will block other events, but it shouldn't take too long... - f, err := os.Open(v.filename) - if err != nil { - screen.PostEvent(NewEventError(err)) - continue - } - books, err := importer.CSVToBooks(f) - if err != nil { - screen.PostEvent(NewEventError(err)) - continue - } - for b := range books { - err = lib.AddBook(context.Background(), &books[b]) - if err != nil { - screen.PostEvent(NewEventError(err)) - } - } - screen.PostEvent(NewEventCloseImport()) - allbooks, err := lib.GetAllBooks(context.Background()) - if err != nil { - screen.PostEvent(NewEventError(err)) - } - state.Set("library", allbooks) - state.Set("ui_state", IN_MENU) - case *EventCloseImport: - state.Set("ui_state", IN_MENU) - screen.HideCursor() - menu.SetStyle(ui.StyleActive) - popup.SetVisible(false) - case *EventError: - errorMessage.SetText(v.err.Error()) - errorPopup.SetVisible(true) - case *EventQuit: - screen.Fini() - fmt.Printf("Thank you for playing Wing Commander!\n\n") - return - case *tcell.EventInterrupt: - case *tcell.EventMouse: - case *tcell.EventTime: - default: - } - // repaint - l.SetMembers(Titles(state.Get("library").([]media.Book))) - container.Draw(screen) - popup.Draw(screen) - errorPopup.Draw(screen) - screen.Show() - } -} - -func Titles(lb []media.Book) []ui.ListKeyValue { - r := []ui.ListKeyValue{} - for i := range lb { - r = append(r, ui.ListKeyValue{ - Key: lb[i].ID, - Value: lb[i].Title, - }) - } - return r -} - -func GetBookByID(id int, lb []media.Book) *media.Book { - for i := range lb { - if lb[i].ID == id { - return &lb[i] - } - } - return &media.Book{} -} diff --git a/go.mod b/go.mod index 497fbf1..492e448 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.24.1 require ( git.yetaga.in/alazyreader/go-openlibrary v0.0.1 - github.com/gdamore/tcell/v2 v2.8.1 github.com/go-sql-driver/mysql v1.9.2 github.com/irlndts/go-discogs v0.3.6 github.com/kelseyhightower/envconfig v1.4.0 @@ -36,7 +35,6 @@ require ( github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gaissmai/bart v0.18.0 // indirect - github.com/gdamore/encoding v1.0.1 // indirect github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect @@ -49,22 +47,16 @@ require ( github.com/gorilla/securecookie v1.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/illarion/gonotify/v3 v3.0.2 // indirect - github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/klauspost/compress v1.17.11 // indirect - github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect github.com/mdlayher/socket v0.5.0 // indirect github.com/miekg/dns v1.1.58 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect @@ -74,7 +66,6 @@ require ( github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 // indirect - github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/x448/float16 v0.8.4 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect diff --git a/go.sum b/go.sum index d425fb4..44f335b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q= +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= @@ -46,6 +48,8 @@ github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NA github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc= +github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -65,20 +69,16 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY= -github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= -github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= -github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= -github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= -github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -87,14 +87,14 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I= +github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= 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/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M= -github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0= github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= @@ -127,10 +127,6 @@ 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= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= @@ -160,23 +156,17 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= -github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca h1:ecjHwH73Yvqf/oIdQ2vxAX+zc6caQsYdPzsxNW1J3G8= -github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= @@ -189,16 +179,14 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= -github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw= -github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 h1:h/41LFTrwMxB9Xvvug0kRdQCU5TlV1+pAMQw0ZtDE3U= github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= -github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs= -github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI= +github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= +github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= @@ -206,18 +194,10 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= @@ -226,94 +206,28 @@ golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9 golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= @@ -324,8 +238,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k= gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM= honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= @@ -334,11 +246,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.82.0 h1:pposomel4h6Je4brJydcdc2ixNQWDaZyGr5v5MdRr/o= -tailscale.com v1.82.0/go.mod h1:iU6kohVzG+bP0/5XjqBAnW8/6nSG/Du++bO+x7VJZD0= -tailscale.com v1.82.5 h1:p5owmyPoPM1tFVHR3LjquFuLfpZLzafvhe5kjVavHtE= -tailscale.com v1.82.5/go.mod h1:iU6kohVzG+bP0/5XjqBAnW8/6nSG/Du++bO+x7VJZD0= -tailscale.com v1.84.0 h1:WzelL3/TXAAN+Vv5UyK0n0JCOL9n0qpjRL4tjVEA1Ok= -tailscale.com v1.84.0/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo= tailscale.com v1.84.1 h1:xtuiYeAIUR+dRztPzzqUsjj+Fv/06vz28zoFaP1k/Os= tailscale.com v1.84.1/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo= diff --git a/ui/mock.go b/ui/mock.go deleted file mode 100644 index 2555529..0000000 --- a/ui/mock.go +++ /dev/null @@ -1,156 +0,0 @@ -package ui - -import ( - "fmt" - - "github.com/gdamore/tcell/v2" -) - -type coord struct { - x, y int -} - -type MockScreen struct { - x, y, h, w int - content map[coord]rune -} - -func (m *MockScreen) Init() error { - m.content = map[coord]rune{} - return nil -} - -func (m *MockScreen) Fini() {} - -func (m *MockScreen) Clear() { - m.content = map[coord]rune{} -} - -func (m *MockScreen) Fill(rune, tcell.Style) {} - -func (m *MockScreen) SetCell(x int, y int, style tcell.Style, ch ...rune) {} - -func (m *MockScreen) GetContent(x, y int) (mainc rune, combc []rune, style tcell.Style, width int) { - return m.content[coord{x, y}], nil, tcell.StyleDefault, 1 -} - -func (m *MockScreen) SetContent(x int, y int, mainc rune, combc []rune, style tcell.Style) { - m.content[coord{x, y}] = mainc -} - -func (m *MockScreen) Suspend() error { - return nil -} - -func (m *MockScreen) Resume() error { - return nil -} - -func (m *MockScreen) SetStyle(style tcell.Style) {} - -func (m *MockScreen) SetCursorStyle(style tcell.CursorStyle, colors ...tcell.Color) {} - -func (m *MockScreen) ShowCursor(x int, y int) {} - -func (m *MockScreen) HideCursor() {} - -func (m *MockScreen) Size() (int, int) { - return m.h, m.w -} - -func (m *MockScreen) PollEvent() tcell.Event { - return tcell.NewEventError(fmt.Errorf("mock error")) -} - -func (m *MockScreen) ChannelEvents(ch chan<- tcell.Event, quit <-chan struct{}) {} - -func (m *MockScreen) HasPendingEvent() bool { - return false -} - -func (m *MockScreen) PostEvent(ev tcell.Event) error { - return nil -} - -func (m *MockScreen) PostEventWait(ev tcell.Event) {} - -func (m *MockScreen) EnableMouse(...tcell.MouseFlags) {} - -func (m *MockScreen) DisableMouse() {} - -func (m *MockScreen) EnablePaste() {} - -func (m *MockScreen) DisablePaste() {} - -func (m *MockScreen) EnableFocus() {} - -func (m *MockScreen) DisableFocus() {} - -func (m *MockScreen) HasMouse() bool { - return false -} - -func (m *MockScreen) Colors() int { - return 0 -} - -func (m *MockScreen) Show() {} - -func (m *MockScreen) Sync() {} - -func (m *MockScreen) CharacterSet() string { - return "UTF-8" -} - -func (m *MockScreen) RegisterRuneFallback(r rune, subst string) {} - -func (m *MockScreen) UnregisterRuneFallback(r rune) {} - -func (m *MockScreen) CanDisplay(r rune, checkFallbacks bool) bool { - return true -} - -func (m *MockScreen) Resize(x, y, h, w int) { - m.x, m.y, m.h, m.w = x, y, h, w -} - -func (m *MockScreen) SetSize(h, w int) { - m.h, m.w = h, w -} - -func (m *MockScreen) HasKey(tcell.Key) bool { - return true -} - -func (m *MockScreen) Beep() error { - return nil -} - -func (m *MockScreen) SetTitle(string) {} - -func (m *MockScreen) GetClipboard() {} - -func (m *MockScreen) SetClipboard([]byte) {} - -func (m *MockScreen) LockRegion(x, y, width, height int, lock bool) {} - -func (m *MockScreen) Tty() (tcell.Tty, bool) { - return nil, false -} - -func (m *MockScreen) DumpContents() string { - var res string - for i := m.y; i < m.h; i++ { - str := []rune{} - for j := m.x; j < m.w; j++ { - r, ok := m.content[coord{x: j, y: i}] - if ok { - str = append(str, r) - } else { - str = append(str, ' ') - } - } - res = res + string(str) + "\n" - } - return res -} diff --git a/ui/ui.go b/ui/ui.go deleted file mode 100644 index d7e6625..0000000 --- a/ui/ui.go +++ /dev/null @@ -1,616 +0,0 @@ -package ui - -import ( - "strconv" - "strings" - - "git.yetaga.in/alazyreader/library/media" - "github.com/gdamore/tcell/v2" -) - -type Drawable interface { - Draw(tcell.Screen) - SetSize(x, y, h, w int) - SetStyle(tcell.Style) - SetVisible(bool) -} - -type Offsets struct { - Top int - Bottom int - Left int - Right int - Percent int -} - -type Contents []struct { - Offsets Offsets - Container Drawable -} - -const ( - LayoutUnmanaged = iota - LayoutHorizontalEven - LayoutVerticalEven - LayoutHorizontalPercent - LayoutVerticalPercent -) - -var ( - StyleActive = tcell.Style{}.Foreground(tcell.ColorWhite).Background(tcell.ColorBlack) - StyleInactive = tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack) -) - -// A Container has no visible UI of its own, but arranges sub-components on the screen. -// layoutMethod manages how subcomponents are organized. If `LayoutUnmanaged`, it just uses the offsets -// in contents to paint on the screen. Otherwise, `LayoutHorizontalEven` and `LayoutVerticalEven` will -// have it compute even distributions of space for all components either horizontally or vertically, -// filling the container. -type Container struct { - x, y int - h, w int - layoutMethod int - contents Contents - visible bool -} - -func NewContainer(contents Contents, layoutMethod int) *Container { - return &Container{ - layoutMethod: layoutMethod, - contents: contents, - visible: true, - } -} - -func (c *Container) Draw(s tcell.Screen) { - if !c.visible { - return - } - for i := range c.contents { - c.contents[i].Container.Draw(s) - } -} - -func (c *Container) SetSize(x, y, h, w int) { - c.x, c.y, c.h, c.w = x, y, h, w - carry := 0 - if c.layoutMethod == LayoutVerticalEven { - num := len(c.contents) - extra := c.h % num - for r := range c.contents { - w := c.w - h := c.h / num - x := c.x - y := c.y + (h * r) + carry - if extra > 0 { // distribute "extra" space to containers as we have some left - h++ - extra-- - carry++ - } - c.contents[r].Container.SetSize(x, y, h, w) - } - } else if c.layoutMethod == LayoutHorizontalEven { - num := len(c.contents) - extra := c.w % num - for r := range c.contents { - w := c.w / num - h := c.h - x := c.x + (w * r) + carry - y := c.y - if extra > 0 { // distribute "extra" space to containers as we have some left - w++ - extra-- - carry++ - } - c.contents[r].Container.SetSize(x, y, h, w) - } - } else if c.layoutMethod == LayoutHorizontalPercent { - // first, work out overall distribution - total := 0 - for r := range c.contents { - // `0` or negatives are set as minimum - if c.contents[r].Offsets.Percent < 1 { - total += 1 - } else { - total += c.contents[r].Offsets.Percent - } - } - carry := 0 - // push around containers - for r := range c.contents { - ratio := (float64(c.contents[r].Offsets.Percent) / float64(total)) - w := int(float64(c.w) * ratio) - h := c.h - x := c.x + carry - y := c.y - carry += w - // and add any remaining space to the last container - if r == len(c.contents)-1 { - w += (c.w - carry) - } - c.contents[r].Container.SetSize(x, y, h, w) - } - } else if c.layoutMethod == LayoutVerticalPercent { - // first, work out overall distribution - total := 0 - for r := range c.contents { - // `0` or negatives are set as minimum - if c.contents[r].Offsets.Percent < 1 { - total += 1 - } else { - total += c.contents[r].Offsets.Percent - } - } - carry := 0 - // push around containers - for r := range c.contents { - ratio := (float64(c.contents[r].Offsets.Percent) / float64(total)) - w := c.w - h := int(float64(c.h) * ratio) - x := c.x - y := c.y + carry - carry += h - // and add any remaining space to the last container - if r == len(c.contents)-1 { - h += (c.h - carry) - } - c.contents[r].Container.SetSize(x, y, h, w) - } - } else { - for r := range c.contents { - x := c.x + c.contents[r].Offsets.Left - y := c.y + c.contents[r].Offsets.Top - h := c.h - c.contents[r].Offsets.Bottom - w := c.w - c.contents[r].Offsets.Right - c.contents[r].Container.SetSize(x, y, h, w) - } - } -} - -func (c *Container) SetStyle(s tcell.Style) { - // containers have no visible elements to style -} - -func (c *Container) SetVisible(b bool) { - c.visible = b -} - -func (c *Container) Contents() Contents { - return c.contents -} - -func (c *Container) SetContents(con Contents) { - c.contents = con -} - -// A Box draws a ASCII box around its contents, with an optional title and footer. -type Box struct { - x, y int - h, w int - title Drawable - menuItems Drawable - contents Contents - style tcell.Style - cascade bool - visible bool - transparent bool -} - -func NewBox(title string, menuItems []string, contents Contents, initialStyle tcell.Style, cascade bool) *Box { - return &Box{ - title: NewPaddedText(title), - menuItems: NewPaddedText(strings.Join(menuItems, " ")), - contents: contents, - style: initialStyle, - cascade: cascade, - visible: true, - transparent: false, - } -} - -func (b *Box) SetSize(x, y, h, w int) { - b.x, b.y, b.h, b.w = x, y, h, w - b.title.SetSize(b.x+2, b.y, 0, 0) - b.menuItems.SetSize(b.x+2, b.y+b.h-1, 0, 0) - for c := range b.contents { - x := b.x + b.contents[c].Offsets.Left - y := b.y + b.contents[c].Offsets.Top - h := b.h - b.contents[c].Offsets.Bottom - w := b.w - b.contents[c].Offsets.Right - b.contents[c].Container.SetSize(x, y, h, w) - } -} - -func (b *Box) Draw(s tcell.Screen) { - if !b.visible { - return - } - // blank out inner area - if !b.transparent { - for m := b.x + 1; m < b.x+b.w-1; m++ { - for n := b.y + 1; n < b.y+b.h-1; n++ { - s.SetContent(m, n, ' ', nil, b.style) - } - } - } - // draw outside bars - for m := b.x + 1; m < b.x+b.w-1; m++ { - s.SetContent(m, b.y, tcell.RuneHLine, nil, b.style) - s.SetContent(m, b.y+b.h-1, tcell.RuneHLine, nil, b.style) - } - for m := b.y + 1; m < b.y+b.h-1; m++ { - s.SetContent(b.x, m, tcell.RuneVLine, nil, b.style) - s.SetContent(b.x+b.w-1, m, tcell.RuneVLine, nil, b.style) - } - s.SetContent(b.x, b.y, tcell.RuneULCorner, nil, b.style) - s.SetContent(b.x+b.w-1, b.y, tcell.RuneURCorner, nil, b.style) - s.SetContent(b.x, b.y+b.h-1, tcell.RuneLLCorner, nil, b.style) - s.SetContent(b.x+b.w-1, b.y+b.h-1, tcell.RuneLRCorner, nil, b.style) - - if b.title != nil { - b.title.Draw(s) - } - if b.menuItems != nil { - b.menuItems.Draw(s) - } - for c := range b.contents { - b.contents[c].Container.Draw(s) - } -} - -func (b *Box) SetStyle(s tcell.Style) { - b.style = s - b.title.SetStyle(s) - b.menuItems.SetStyle(s) - if b.cascade { - for c := range b.contents { - b.contents[c].Container.SetStyle(s) - } - } -} - -func (b *Box) SetVisible(v bool) { - b.visible = v -} - -func (b *Box) SetTransparent(v bool) { - b.transparent = v -} - -func (b *Box) Contents() Contents { - return b.contents -} - -func (b *Box) SetContents(c Contents) { - b.contents = c -} - -// A List is a scrollable, pageable list with a selector token. -type List struct { - x, y int - h, w int - selected int - listItems []ListKeyValue - style tcell.Style - visible bool -} - -type ListKeyValue struct { - Key int - Value string -} - -func NewList(listItems []ListKeyValue, initialSelected int) *List { - return &List{ - listItems: listItems, - selected: initialSelected, - visible: true, - } -} - -func (l *List) SetSize(x, y, h, w int) { - l.x, l.y, l.h, l.w = x, y, h, w -} - -func (l *List) Draw(s tcell.Screen) { - if !l.visible { - return - } - for i := range l.listItems { - for j, r := range l.listItems[i].Value { - s.SetContent(l.x+j, l.y+i, r, nil, l.style) - } - if i == l.selected { - s.SetContent(l.x+len(l.listItems[i].Value)+1, l.y+i, '<', nil, l.style) - } - } -} - -func (l *List) SetVisible(b bool) { - l.visible = b -} - -func (l *List) SetStyle(s tcell.Style) { - l.style = s -} - -func (l *List) Selected() int { - return l.selected -} - -func (l *List) SelectedID() int { - if l.listItems == nil || len(l.listItems) == 0 { - return 0 - } - return l.listItems[l.selected].Key -} - -func (l *List) SetSelected(i int) { - l.selected = i -} - -func (l *List) ListMembers() []ListKeyValue { - return l.listItems -} - -func (l *List) SetMembers(lkv []ListKeyValue) { - l.listItems = lkv -} - -// BookDetails displays an editable list of book details -type BookDetails struct { - x, y int - h, w int - book *media.Book - style tcell.Style - visible bool -} - -func NewBookDetails(b *media.Book) *BookDetails { - return &BookDetails{ - book: b, - visible: true, - } -} - -func (l *BookDetails) SetBook(b *media.Book) { - l.book = b -} - -func (l *BookDetails) SetSize(x, y, h, w int) { - l.x, l.y, l.h, l.w = x, y, h, w -} - -func (l *BookDetails) Draw(s tcell.Screen) { - if l.book == nil { - return - } - if !l.visible { - return - } - items := []struct { - label string - value string - }{ - {"Title", l.book.Title}, - {"Authors", strings.Join(l.book.Authors, ", ")}, - {"Sort Author", l.book.SortAuthor}, - {"ISBN-10", l.book.ISBN10}, - {"ISBN-13", l.book.ISBN13}, - {"Format", l.book.Format}, - {"Genre", l.book.Genre}, - {"Publisher", l.book.Publisher}, - {"Series", l.book.Series}, - {"Volume", l.book.Volume}, - {"Year", l.book.Year}, - {"Signed", strconv.FormatBool(l.book.Signed)}, - {"Cover URL", l.book.CoverURL}, - {"Notes", l.book.Notes}, - {"Description", l.book.Description}, - } - for i := range items { - if i < l.h-2 { - kv := NewKeyValue(items[i].label, ": ", items[i].value) - kv.SetSize(l.x, l.y+i, 0, 0) - kv.SetStyle(l.style) - kv.Draw(s) - } - } -} - -func (l *BookDetails) SetVisible(b bool) { - l.visible = b -} - -func (l *BookDetails) SetStyle(s tcell.Style) { - l.style = s -} - -// PaddedText outputs strings with a space on both sides. -// Useful for generating headings, footers, etc. Used by Box. -type PaddedText struct { - x, y int - h, w int - text string - style tcell.Style - visible bool -} - -func NewPaddedText(text string) *PaddedText { - return &PaddedText{text: text, visible: true} -} - -func (p *PaddedText) SetSize(x, y, _, _ int) { - p.x, p.y, p.h, p.w = x, y, 1, len(p.text)+2 -} - -func (p *PaddedText) SetStyle(s tcell.Style) { - p.style = s -} - -func (p *PaddedText) Draw(s tcell.Screen) { - if p.text == "" { - return - } - if !p.visible { - return - } - t := p.x - s.SetContent(t, p.y, ' ', nil, p.style) - t++ - for _, r := range p.text { - s.SetContent(t, p.y, r, nil, p.style) - t++ - } - s.SetContent(t, p.y, ' ', nil, p.style) -} - -func (p *PaddedText) SetVisible(b bool) { - p.visible = b -} - -type KeyValue struct { - x, y int - h, w int - key string - value string - separator string - style tcell.Style - visible bool -} - -func NewKeyValue(key, separator, value string) *KeyValue { - return &KeyValue{ - key: key, - separator: separator, - value: value, - visible: true, - } -} - -func (p *KeyValue) SetSize(x, y, _, _ int) { - p.x, p.y, p.h, p.w = x, y, 1, len(p.key)+len(p.separator)+len(p.value) -} - -func (p *KeyValue) SetStyle(s tcell.Style) { - p.style = s -} - -func (p *KeyValue) Draw(s tcell.Screen) { - if !p.visible { - return - } - for j, r := range p.key { - s.SetContent(p.x+j, p.y, r, nil, p.style) - } - for j, r := range p.separator { - s.SetContent(p.x+len(p.key)+j, p.y, r, nil, p.style) - } - for j, r := range p.value { - s.SetContent(p.x+len(p.key)+len(p.separator)+j, p.y, r, nil, p.style) - } -} - -func (p *KeyValue) SetVisible(b bool) { - p.visible = b -} - -func (p *KeyValue) GetValue() string { - return p.value -} - -type EditableTextLine struct { - x, y int - h, w int - text string - style tcell.Style - visible bool - cursorPos int - showCursor bool -} - -func NewEditableTextLine(initialText string) *EditableTextLine { - return &EditableTextLine{ - text: initialText, - visible: true, - showCursor: true, - } -} - -func (p *EditableTextLine) SetSize(x, y, _, _ int) { - p.x, p.y, p.h, p.w = x, y, 1, len(p.text) -} - -func (p *EditableTextLine) SetStyle(s tcell.Style) { - p.style = s -} - -func (p *EditableTextLine) Draw(s tcell.Screen) { - if !p.visible { - return - } - for j, r := range p.text { - s.SetContent(p.x+j, p.y, r, nil, p.style) - } - s.ShowCursor(p.x+p.cursorPos, p.y) -} - -func (p *EditableTextLine) SetVisible(b bool) { - p.visible = b -} - -func (p *EditableTextLine) SetCursorVisible(b bool) { - p.showCursor = b -} - -func (p *EditableTextLine) SetText(t string) { - p.text = t - if len(p.text) == 0 { - p.ResetCursor(true) - return - } - p.ResetCursor(false) -} - -func (p *EditableTextLine) Text() string { - return p.text -} - -func (p *EditableTextLine) ResetCursor(beginning bool) { - if beginning { - p.cursorPos = 0 - } else { - p.cursorPos = len(p.text) - } -} - -func (p *EditableTextLine) InsertAtCursor(r rune) { - if len(p.text) == 0 { - p.text = string(r) - p.cursorPos = 1 - return - } - p.text = p.text[0:p.cursorPos] + string(r) + p.text[p.cursorPos:len(p.text)] - p.cursorPos = p.cursorPos + 1 -} - -func (p *EditableTextLine) MoveCursor(i int) { - if p.cursorPos+i < 0 { - p.cursorPos = 0 - return - } - if p.cursorPos+i > len(p.text) { - p.cursorPos = len(p.text) - return - } - p.cursorPos = p.cursorPos + i -} - -func (p *EditableTextLine) DeleteAtCursor() { - if len(p.text) == 0 { - p.cursorPos = 0 - return - } - p.text = p.text[0:p.cursorPos-1] + p.text[p.cursorPos:len(p.text)] - p.cursorPos = p.cursorPos - 1 -} diff --git a/ui/ui_test.go b/ui/ui_test.go deleted file mode 100644 index fa4db7a..0000000 --- a/ui/ui_test.go +++ /dev/null @@ -1,241 +0,0 @@ -package ui - -import ( - "fmt" - "testing" - - "github.com/gdamore/tcell/v2" -) - -func TestContainerOneBox(t *testing.T) { - expect := `┌─ box one ────────┐ -│ │ -│ │ -│ │ -└──────────────────┘ -` - m := &MockScreen{} - one := NewBox("box one", nil, Contents{}, tcell.Style{}, false) - container := NewContainer( - Contents{{Container: one}}, - LayoutHorizontalEven, - ) - m.Init() - m.Resize(0, 0, 5, 20) - container.SetSize(0, 0, 5, 20) - container.Draw(m) - result := m.DumpContents() - if result != expect { - fmt.Printf("expected:\n%+v", expect) - fmt.Printf("actual:\n%+v", result) - t.Fail() - } -} - -func TestContainerTwoBoxesHStack(t *testing.T) { - expect := `┌─ one ──┐┌─ two ──┐ -│ ││ │ -│ ││ │ -│ ││ │ -└────────┘└────────┘ -` - m := &MockScreen{} - one := NewBox("one", nil, Contents{}, tcell.Style{}, false) - two := NewBox("two", nil, Contents{}, tcell.Style{}, false) - container := NewContainer( - Contents{{Container: one}, {Container: two}}, - LayoutHorizontalEven, - ) - m.Init() - m.Resize(0, 0, 5, 20) - container.SetSize(0, 0, 5, 20) - container.Draw(m) - result := m.DumpContents() - if result != expect { - fmt.Printf("expected:\n%+v", expect) - fmt.Printf("actual:\n%+v", result) - t.Fail() - } -} - -func TestContainerThreeBoxesUnevenHStack(t *testing.T) { - expect := `┌─ one ──┐┌─ two ──┐┌─ three -│ ││ ││ │ -│ ││ ││ │ -│ ││ ││ │ -└────────┘└────────┘└───────┘ -` - m := &MockScreen{} - one := NewBox("one", nil, Contents{}, tcell.Style{}, false) - two := NewBox("two", nil, Contents{}, tcell.Style{}, false) - three := NewBox("three", nil, Contents{}, tcell.Style{}, false) - container := NewContainer( - Contents{{Container: one}, {Container: two}, {Container: three}}, - LayoutHorizontalEven, - ) - m.Init() - m.Resize(0, 0, 5, 29) - container.SetSize(0, 0, 5, 29) - container.Draw(m) - result := m.DumpContents() - if result != expect { - fmt.Printf("expected:\n%+v", expect) - fmt.Printf("actual:\n%+v", result) - t.Fail() - } -} - -func TestContainerTwoBoxesHPercentStack(t *testing.T) { - expect := `┌─ one ──────┐┌─ two ┐ -│ ││ │ -│ ││ │ -│ ││ │ -└────────────┘└──────┘ -` - m := &MockScreen{} - one := NewBox("one", nil, Contents{}, tcell.Style{}, false) - two := NewBox("two", nil, Contents{}, tcell.Style{}, false) - container := NewContainer( - Contents{ - {Container: one, Offsets: Offsets{Percent: 2}}, - {Container: two, Offsets: Offsets{Percent: 1}}}, - LayoutHorizontalPercent, - ) - m.Init() - m.Resize(0, 0, 5, 22) - container.SetSize(0, 0, 5, 22) - container.Draw(m) - result := m.DumpContents() - if result != expect { - fmt.Printf("expected:\n%+v", expect) - fmt.Printf("actual:\n%+v", result) - t.Fail() - } -} - -func TestContainerTwoBoxesVStack(t *testing.T) { - expect := `┌─ one ──┐ -│ │ -│ │ -│ │ -└────────┘ -┌─ two ──┐ -│ │ -│ │ -│ │ -└────────┘ -` - m := &MockScreen{} - one := NewBox("one", nil, Contents{}, tcell.Style{}, false) - two := NewBox("two", nil, Contents{}, tcell.Style{}, false) - container := NewContainer( - Contents{{Container: one}, {Container: two}}, - LayoutVerticalEven, - ) - m.Init() - m.Resize(0, 0, 10, 10) - container.SetSize(0, 0, 10, 10) - container.Draw(m) - result := m.DumpContents() - if result != expect { - fmt.Printf("expected:\n%+v", expect) - fmt.Printf("actual:\n%+v", result) - t.Fail() - } -} - -func TestContainerTwoBoxesPercentageVStack(t *testing.T) { - expect := `┌─ one ──┐ -│ │ -│ │ -│ │ -│ │ -└────────┘ -┌─ two ──┐ -│ │ -│ │ -└────────┘ -` - m := &MockScreen{} - one := NewBox("one", nil, Contents{}, tcell.Style{}, false) - two := NewBox("two", nil, Contents{}, tcell.Style{}, false) - container := NewContainer( - Contents{ - {Container: one, Offsets: Offsets{Percent: 2}}, - {Container: two, Offsets: Offsets{Percent: 1}}}, - LayoutVerticalPercent, - ) - m.Init() - m.Resize(0, 0, 10, 10) - container.SetSize(0, 0, 10, 10) - container.Draw(m) - result := m.DumpContents() - if result != expect { - fmt.Printf("expected:\n%+v", expect) - fmt.Printf("actual:\n%+v", result) - t.Fail() - } -} - -func TestNewEditableTextLine(t *testing.T) { - e := NewEditableTextLine("") - e.InsertAtCursor('a') - e.InsertAtCursor('b') - e.InsertAtCursor('c') - if e.text != "abc" { - fmt.Printf("expected: 'abc', actual: '%+v'", e.text) - t.Fail() - } - e.MoveCursor(-1) - e.InsertAtCursor('d') - if e.text != "abdc" { - fmt.Printf("expected: 'abdc', actual: '%+v'", e.text) - t.Fail() - } - e.MoveCursor(-20) - e.InsertAtCursor('e') - if e.text != "eabdc" { - fmt.Printf("expected: 'eabdc', actual: '%+v'", e.text) - t.Fail() - } - e.MoveCursor(20) - e.InsertAtCursor('f') - if e.text != "eabdcf" { - fmt.Printf("expected: 'eabdcf', actual: '%+v'", e.text) - t.Fail() - } - e.MoveCursor(1) - e.InsertAtCursor('g') - if e.text != "eabdcfg" { - fmt.Printf("expected: 'eabdcfg', actual: '%+v'", e.text) - t.Fail() - } - e.DeleteAtCursor() - e.DeleteAtCursor() - e.MoveCursor(-1) - e.DeleteAtCursor() - if e.text != "eabc" { - fmt.Printf("expected: 'eabc', actual: '%+v'", e.text) - t.Fail() - } - e.ResetCursor(false) - e.InsertAtCursor('h') - e.ResetCursor(true) - e.InsertAtCursor('g') - if e.text != "geabch" { - fmt.Printf("expected: 'geabch', actual: '%+v'", e.text) - t.Fail() - } - e.SetText("the rain in spain") - e.InsertAtCursor('s') - if e.text != "the rain in spains" { - fmt.Printf("expected: 'the rain in spains', actual: '%+v'", e.text) - t.Fail() - } - e.SetText("") - e.InsertAtCursor('s') - if e.text != "s" { - fmt.Printf("expected: 's', actual: '%+v'", e.text) - t.Fail() - } -}