Merge branch 'main' of ssh://git.bullercodeworks.com:2200/brian/expds
This commit is contained in:
@@ -23,8 +23,10 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.bullercodeworks.com/brian/expds/data"
|
"git.bullercodeworks.com/brian/expds/data"
|
||||||
"git.bullercodeworks.com/brian/expds/data/models"
|
"git.bullercodeworks.com/brian/expds/data/models"
|
||||||
@@ -43,8 +45,7 @@ type ScreenHome struct {
|
|||||||
w, h int
|
w, h int
|
||||||
style tcell.Style
|
style tcell.Style
|
||||||
|
|
||||||
alert *w.Alert
|
alert *w.Alert
|
||||||
showAlert bool
|
|
||||||
|
|
||||||
menuLayout *w.TopMenuLayout
|
menuLayout *w.TopMenuLayout
|
||||||
|
|
||||||
@@ -215,9 +216,6 @@ func (s *ScreenHome) Draw() {
|
|||||||
s.a.DrawWidget(s.loading)
|
s.a.DrawWidget(s.loading)
|
||||||
}
|
}
|
||||||
s.a.DrawWidget(s.status)
|
s.a.DrawWidget(s.status)
|
||||||
if s.showAlert {
|
|
||||||
s.a.DrawWidget(s.alert)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
func (s *ScreenHome) Exit() error { return nil }
|
func (s *ScreenHome) Exit() error { return nil }
|
||||||
func (s *ScreenHome) Log(t string, a ...any) {
|
func (s *ScreenHome) Log(t string, a ...any) {
|
||||||
@@ -318,9 +316,11 @@ func (s *ScreenHome) initCli() {
|
|||||||
func (s *ScreenHome) toggleCli() {
|
func (s *ScreenHome) toggleCli() {
|
||||||
if s.layout.Contains(s.cli) {
|
if s.layout.Contains(s.cli) {
|
||||||
s.layout.Delete(s.cli)
|
s.layout.Delete(s.cli)
|
||||||
|
s.layout.ActivateWidget(s.columns)
|
||||||
} else {
|
} else {
|
||||||
s.layout.Add(s.cli)
|
s.layout.Add(s.cli)
|
||||||
s.cli.SetVisible(true)
|
s.cli.SetVisible(true)
|
||||||
|
s.layout.ActivateWidget(s.cli)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (s *ScreenHome) hideCli() {
|
func (s *ScreenHome) hideCli() {
|
||||||
@@ -346,7 +346,13 @@ func (s *ScreenHome) cliGetPds(args ...string) bool {
|
|||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { s.isLoading = false }()
|
defer func() { s.isLoading = false }()
|
||||||
pds, err := s.r.GetPDS(args[1])
|
var pds *models.Pds
|
||||||
|
var err error
|
||||||
|
if s.activePds != nil && string(s.activePds.AtId) == args[1] {
|
||||||
|
pds, err = s.r.ReloadPds(args[1])
|
||||||
|
} else {
|
||||||
|
pds, err = s.r.GetPDS(args[1])
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log(err.Error())
|
s.Log(err.Error())
|
||||||
return
|
return
|
||||||
@@ -355,7 +361,7 @@ func (s *ScreenHome) cliGetPds(args ...string) bool {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.doOpen = false
|
s.doOpen = false
|
||||||
s.cli.Log("Retrieved: %s (%s)", pds.AtId, pds.Did)
|
slog.Default().Debug(fmt.Sprintf("Retrieved: %s (%s) at %s", pds.AtId, pds.Did, time.Now().Format(time.RFC3339)))
|
||||||
s.activePds = pds
|
s.activePds = pds
|
||||||
s.updatePdsListing()
|
s.updatePdsListing()
|
||||||
n, err := s.pdsListing.GetActiveNode()
|
n, err := s.pdsListing.GetActiveNode()
|
||||||
@@ -425,11 +431,14 @@ func (s *ScreenHome) cliSendStatus(args ...string) bool {
|
|||||||
s.isLoading = true
|
s.isLoading = true
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { s.isLoading = false }()
|
defer func() { s.isLoading = false }()
|
||||||
if !s.r.Auth.HasAuth() {
|
if !s.r.Auth.HasAuth(s.activePds.Did) {
|
||||||
s.Log("Not authorized. Run `authpds`")
|
s.Log("Not authorized. Run `authpds`")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.r.SendToPDS()
|
err := s.r.SendToPDS(s.activePds.Did)
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error sending status: %w", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -438,6 +447,7 @@ func (s *ScreenHome) updatePdsListing() {
|
|||||||
s.pdsListing.SetTitle(fmt.Sprintf("─ %s (%s)", s.activePds.AtId.String(), s.activePds.Did.String()))
|
s.pdsListing.SetTitle(fmt.Sprintf("─ %s (%s)", s.activePds.AtId.String(), s.activePds.Did.String()))
|
||||||
s.pdsListing.Clear()
|
s.pdsListing.Clear()
|
||||||
nsidList := s.activePds.NSIDStringList()
|
nsidList := s.activePds.NSIDStringList()
|
||||||
|
var tree []*wd.TreeNode
|
||||||
for i, v := range nsidList {
|
for i, v := range nsidList {
|
||||||
t := wd.NewTreeNode(v, v)
|
t := wd.NewTreeNode(v, v)
|
||||||
nsid := s.activePds.NSIDs[i]
|
nsid := s.activePds.NSIDs[i]
|
||||||
@@ -460,8 +470,9 @@ func (s *ScreenHome) updatePdsListing() {
|
|||||||
c := wd.NewTreeNode(label, rIds[j])
|
c := wd.NewTreeNode(label, rIds[j])
|
||||||
t.AddChild(c)
|
t.AddChild(c)
|
||||||
}
|
}
|
||||||
s.pdsListing.Add(t)
|
tree = append(tree, t)
|
||||||
}
|
}
|
||||||
|
s.pdsListing.SetTree(tree)
|
||||||
s.layout.ActivateWidget(s.columns)
|
s.layout.ActivateWidget(s.columns)
|
||||||
s.columns.ActivateWidget(s.pdsListing)
|
s.columns.ActivateWidget(s.pdsListing)
|
||||||
s.pdsListing.SetActive(true)
|
s.pdsListing.SetActive(true)
|
||||||
|
|||||||
531
app/screen_home.go.orig
Normal file
531
app/screen_home.go.orig
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
/*
|
||||||
|
Copyright © Brian Buller <brian@bullercodeworks.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.bullercodeworks.com/brian/expds/data"
|
||||||
|
"git.bullercodeworks.com/brian/expds/data/models"
|
||||||
|
wd "git.bullercodeworks.com/brian/expds/widgets"
|
||||||
|
w "git.bullercodeworks.com/brian/tcell-widgets"
|
||||||
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"golang.design/x/clipboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScreenHome struct {
|
||||||
|
a *App
|
||||||
|
r *data.Repo
|
||||||
|
w, h int
|
||||||
|
style tcell.Style
|
||||||
|
|
||||||
|
alert *w.Alert
|
||||||
|
|
||||||
|
menuLayout *w.TopMenuLayout
|
||||||
|
|
||||||
|
openPdsEntry *w.Field
|
||||||
|
|
||||||
|
layout *w.LinearLayout
|
||||||
|
columns *w.LinearLayout
|
||||||
|
|
||||||
|
activePds *models.Pds
|
||||||
|
pdsListing *wd.TreeBrowser
|
||||||
|
jsonContent *wd.JsonContent
|
||||||
|
status *wd.StatusBar
|
||||||
|
stPathBlock *wd.StatusBlock
|
||||||
|
|
||||||
|
cli *w.Cli
|
||||||
|
|
||||||
|
doOpen bool
|
||||||
|
isLoading bool
|
||||||
|
loading *w.Spinner
|
||||||
|
|
||||||
|
cursor int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) Init(a *App) {
|
||||||
|
s.a, s.r = a, a.repo
|
||||||
|
s.r.SetLogFunc(s.Log)
|
||||||
|
|
||||||
|
s.style = a.style
|
||||||
|
|
||||||
|
s.alert = w.NewAlert("expds.alert", s.style)
|
||||||
|
|
||||||
|
s.openPdsEntry = w.NewField("home.openpds.field", s.style)
|
||||||
|
s.openPdsEntry.SetLabel("ID")
|
||||||
|
s.openPdsEntry.SetActive(true)
|
||||||
|
s.openPdsEntry.SetPos(w.Coord{X: 3, Y: 1})
|
||||||
|
s.openPdsEntry.SetSize(w.Coord{X: 20, Y: 1})
|
||||||
|
s.doOpen = true
|
||||||
|
|
||||||
|
s.isLoading = false
|
||||||
|
s.loading = w.NewSpinner("home.loading", s.style)
|
||||||
|
s.loading.SetVisible(true)
|
||||||
|
s.loading.SetPos(w.Coord{X: 2, Y: 1})
|
||||||
|
|
||||||
|
s.menuLayout = w.NewTopMenuLayout("home.toplayout", s.style)
|
||||||
|
s.initMenu()
|
||||||
|
|
||||||
|
s.cli = w.NewCli("home.cli", s.style)
|
||||||
|
s.initCli()
|
||||||
|
s.cli.SetVisible(false)
|
||||||
|
|
||||||
|
s.layout = w.NewLinearLayout("home.layout", s.style)
|
||||||
|
|
||||||
|
s.columns = w.NewLinearLayout("home.layout.columns", s.style)
|
||||||
|
s.columns.SetOrientation(w.LinLayH)
|
||||||
|
|
||||||
|
//s.pdsListing = w.NewSimpleListWithHelp("pdslisting", s.style)
|
||||||
|
s.pdsListing = wd.NewTreeBrowser("pdslisting", s.style)
|
||||||
|
s.pdsListing.SetBorder([]rune{'─', '┬', '│', '┴', '─', '└', '│', '┌', '├', '─', '┤', '┬', '│', '┴', '┼'})
|
||||||
|
|
||||||
|
s.pdsListing.SetTitle(strings.Repeat(" ", 30))
|
||||||
|
s.pdsListing.SetOnSelect(s.selectPdsListingEntry)
|
||||||
|
s.pdsListing.SetOnChange(s.changePdsList)
|
||||||
|
s.pdsListing.SetVimMode(viper.GetBool(data.KeyVimMode))
|
||||||
|
s.pdsListing.SetLogger(s.Log)
|
||||||
|
|
||||||
|
s.jsonContent = wd.NewJsonContent("jsoncontent", s.style)
|
||||||
|
km := s.jsonContent.GetKeyMap()
|
||||||
|
km.Add(
|
||||||
|
w.NewKey(w.BuildEK(tcell.KeyEnter), func(ev *tcell.EventKey) bool {
|
||||||
|
// Init returns an error if the package is not ready for use.
|
||||||
|
err := clipboard.Init()
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error initializing clipboard: %s", err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
clipboard.Write(clipboard.FmtText, []byte(s.jsonContent.GetSelectedValue()))
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
w.NewKey(w.BuildEKr('O'), func(ev *tcell.EventKey) bool {
|
||||||
|
url := fmt.Sprintf("https://%s/xrpc/com.atproto.sync.getBlob?did=did:plc:pqwuemo2ic5tqmpwrajb2phi&cid=%s", s.activePds.AtId.String(), s.jsonContent.GetSelectedValue())
|
||||||
|
open.Run(url)
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
s.jsonContent.SetKeyMap(km)
|
||||||
|
s.jsonContent.SetBorder(
|
||||||
|
[]rune{'─', '┐', '│', '┘', '─', '─', ' ', '─', '├', '─', '┤', '┬', '│', '┴', '┼'},
|
||||||
|
)
|
||||||
|
|
||||||
|
statusStyle := s.style.Background(tcell.ColorDarkSlateGray)
|
||||||
|
s.status = wd.NewStatusBar("home.statusbar", statusStyle)
|
||||||
|
s.status.SetPos(w.Coord{X: 0, Y: s.a.GetH() - 1})
|
||||||
|
s.status.SetLogger(s.Log)
|
||||||
|
s.stPathBlock = wd.NewStatusBlock("home.statusbar.block", statusStyle.Foreground(tcell.ColorDarkSlateGray).Background(tcell.ColorOrange))
|
||||||
|
s.stPathBlock.SetType(wd.SBTypePath)
|
||||||
|
s.stPathBlock.SetParts([]string{"No PDS Loaded"})
|
||||||
|
s.status.Add(s.stPathBlock)
|
||||||
|
|
||||||
|
s.columns.AddAll(s.pdsListing, s.jsonContent)
|
||||||
|
|
||||||
|
s.layout.AddAll(s.columns)
|
||||||
|
s.layout.SetLogger(s.Log)
|
||||||
|
s.layout.SetWeight(s.columns, 4)
|
||||||
|
s.menuLayout.SetWidget(s.layout)
|
||||||
|
s.layout.SetLogger(s.Log)
|
||||||
|
s.columns.SetLogger(s.Log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) GetName() string { return "home" }
|
||||||
|
func (s *ScreenHome) HandleResize(ev *tcell.EventResize) {
|
||||||
|
s.w, s.h = ev.Size()
|
||||||
|
s.menuLayout.HandleResize(w.Coord{X: s.w, Y: s.h - 1}.ResizeEvent())
|
||||||
|
s.alert.HandleResize(ev)
|
||||||
|
|
||||||
|
s.status.SetPos(w.Coord{X: 0, Y: s.h - 1})
|
||||||
|
s.status.HandleResize(w.Coord{X: s.w, Y: 1}.ResizeEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) HandleKey(ev *tcell.EventKey) bool {
|
||||||
|
<<<<<<< HEAD
|
||||||
|
if s.showAlert {
|
||||||
|
handled := s.alert.HandleKey(ev)
|
||||||
|
s.alert.SetMessage(fmt.Sprintf("Alert Handled? %v -> %s", handled, ev.Name()))
|
||||||
|
return handled
|
||||||
|
}
|
||||||
|
=======
|
||||||
|
>>>>>>> 367d62ff009186e5aa584fd069dd57aaf7d46a8a
|
||||||
|
if ev.Key() == tcell.KeyF12 {
|
||||||
|
s.toggleCli()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
m := s.menuLayout.Menu()
|
||||||
|
if ev.Key() == tcell.KeyEscape {
|
||||||
|
return s.menuLayout.HandleKey(ev)
|
||||||
|
} else if m.Active() {
|
||||||
|
return s.menuLayout.HandleKey(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.doOpen {
|
||||||
|
if ev.Key() == tcell.KeyCtrlU {
|
||||||
|
s.openPdsEntry.SetValue("")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ev.Key() == tcell.KeyEnter {
|
||||||
|
return s.cliGetPds("getpds", s.openPdsEntry.Value())
|
||||||
|
}
|
||||||
|
return s.openPdsEntry.HandleKey(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.menuLayout.HandleKey(ev)
|
||||||
|
}
|
||||||
|
func (s *ScreenHome) HandleTime(ev *tcell.EventTime) {
|
||||||
|
s.menuLayout.HandleTime(ev)
|
||||||
|
s.loading.HandleTime(ev)
|
||||||
|
s.status.HandleTime(ev)
|
||||||
|
}
|
||||||
|
func (s *ScreenHome) Draw() {
|
||||||
|
if s.doOpen {
|
||||||
|
s.menuLayout.SetStyle(s.style.Foreground(tcell.ColorGray))
|
||||||
|
}
|
||||||
|
s.menuLayout.SetStyle(s.style)
|
||||||
|
m := s.menuLayout.Menu()
|
||||||
|
s.a.DrawWidget(s.menuLayout)
|
||||||
|
if s.doOpen && !m.Active() {
|
||||||
|
s.a.DrawWidget(s.openPdsEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are outside of the menuLayout
|
||||||
|
if s.isLoading {
|
||||||
|
s.a.DrawWidget(s.loading)
|
||||||
|
}
|
||||||
|
s.a.DrawWidget(s.status)
|
||||||
|
}
|
||||||
|
func (s *ScreenHome) Exit() error { return nil }
|
||||||
|
func (s *ScreenHome) Log(t string, a ...any) {
|
||||||
|
s.cli.Log(t, a...)
|
||||||
|
s.showCli()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) initMenu() {
|
||||||
|
s.menuLayout.SetActive(true)
|
||||||
|
wrk := "[ ]"
|
||||||
|
if viper.GetBool(data.KeyVimMode) {
|
||||||
|
wrk = "[X]"
|
||||||
|
}
|
||||||
|
vimText := fmt.Sprintf("%s Vim Mode", wrk)
|
||||||
|
wrk = "[ ]"
|
||||||
|
if viper.GetBool(data.KeyRecNmInfer) {
|
||||||
|
wrk = "[X]"
|
||||||
|
}
|
||||||
|
inferRecNm := fmt.Sprintf("%s Infer Record Names", wrk)
|
||||||
|
s.menuLayout.AddMenuItems(
|
||||||
|
s.menuLayout.CreateMenuItem("file", "File", nil, 'f',
|
||||||
|
s.menuLayout.CreateMenuItem("file.openpds", "Open PDS", func() bool {
|
||||||
|
s.menuLayout.ToggleMenu()
|
||||||
|
s.doOpen = true
|
||||||
|
s.pdsListing.SetTitle(strings.Repeat(" ", 30))
|
||||||
|
return true
|
||||||
|
}, 'o'),
|
||||||
|
s.menuLayout.CreateMenuItem("file.reloadpds", "Reload PDS", func() bool {
|
||||||
|
s.menuLayout.ToggleMenu()
|
||||||
|
if s.activePds == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.cliGetPds("getpds", s.activePds.AtId.String())
|
||||||
|
}, 'r'),
|
||||||
|
s.menuLayout.CreateMenuItem("file.exit", "Exit", func() bool {
|
||||||
|
s.a.Exit()
|
||||||
|
return true
|
||||||
|
}, 'x'),
|
||||||
|
),
|
||||||
|
s.menuLayout.CreateMenuItem("settings", "Settings", nil, 's',
|
||||||
|
s.menuLayout.CreateMenuItem("settings.vimmode", vimText, func() bool {
|
||||||
|
s.menuLayout.ToggleMenu()
|
||||||
|
viper.Set(data.KeyVimMode, !viper.GetBool(data.KeyVimMode))
|
||||||
|
viper.WriteConfig()
|
||||||
|
s.update()
|
||||||
|
return true
|
||||||
|
}, 'v'),
|
||||||
|
s.menuLayout.CreateMenuItem("settings.inferrecnm", inferRecNm, func() bool {
|
||||||
|
s.menuLayout.ToggleMenu()
|
||||||
|
viper.Set(data.KeyRecNmInfer, !viper.GetBool(data.KeyRecNmInfer))
|
||||||
|
viper.WriteConfig()
|
||||||
|
s.update()
|
||||||
|
return true
|
||||||
|
}, 'r'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) update() {
|
||||||
|
if s.doOpen {
|
||||||
|
s.menuLayout.SetWidget(s.openPdsEntry)
|
||||||
|
} else {
|
||||||
|
s.menuLayout.SetWidget(s.layout)
|
||||||
|
}
|
||||||
|
s.pdsListing.SetVimMode(viper.GetBool(data.KeyVimMode))
|
||||||
|
s.jsonContent.SetVimMode(viper.GetBool(data.KeyVimMode))
|
||||||
|
vimMI := s.menuLayout.FindItem("settings.vimmode")
|
||||||
|
wrk := "[ ]"
|
||||||
|
if viper.GetBool(data.KeyVimMode) {
|
||||||
|
wrk = "[X]"
|
||||||
|
}
|
||||||
|
vimMI.SetLabel(fmt.Sprintf("%s Vim Mode", wrk))
|
||||||
|
recNmInf := s.menuLayout.FindItem("settings.inferrecnm")
|
||||||
|
wrk = "[ ]"
|
||||||
|
if viper.GetBool(data.KeyRecNmInfer) {
|
||||||
|
wrk = "[X]"
|
||||||
|
}
|
||||||
|
recNmInf.SetLabel(fmt.Sprintf("%s Infer Record Names", wrk))
|
||||||
|
|
||||||
|
miReload := s.menuLayout.FindItem("file.reloadpds")
|
||||||
|
if s.activePds == nil {
|
||||||
|
miReload.SetDisabled(true)
|
||||||
|
miReload.SetStyle(s.style.Foreground(tcell.ColorGray))
|
||||||
|
} else {
|
||||||
|
miReload.SetDisabled(false)
|
||||||
|
miReload.SetStyle(s.style.Foreground(tcell.ColorLime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) initCli() {
|
||||||
|
s.cli.SetVisible(true)
|
||||||
|
s.cli.AddCommand(w.NewCliCommand("getpds", s.cliGetPds))
|
||||||
|
s.cli.AddCommand(w.NewCliCommand("authpds", s.cliAuthPds))
|
||||||
|
s.cli.AddCommand(w.NewCliCommand("backuppds", s.cliBackupPds))
|
||||||
|
s.cli.AddCommand(w.NewCliCommand("sendstatus", s.cliSendStatus))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) toggleCli() {
|
||||||
|
if s.layout.Contains(s.cli) {
|
||||||
|
s.layout.Delete(s.cli)
|
||||||
|
s.layout.ActivateWidget(s.columns)
|
||||||
|
} else {
|
||||||
|
s.layout.Add(s.cli)
|
||||||
|
s.cli.SetVisible(true)
|
||||||
|
s.layout.ActivateWidget(s.cli)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *ScreenHome) hideCli() {
|
||||||
|
s.cli.SetVisible(false)
|
||||||
|
if s.layout.Contains(s.cli) {
|
||||||
|
s.layout.Delete(s.cli)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *ScreenHome) showCli() {
|
||||||
|
s.cli.SetVisible(true)
|
||||||
|
if !s.layout.Contains(s.cli) {
|
||||||
|
s.layout.Add(s.cli)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) cliGetPds(args ...string) bool {
|
||||||
|
s.isLoading = true
|
||||||
|
if len(args) < 1 {
|
||||||
|
s.Log("No id given.")
|
||||||
|
s.Log("Usage: 'getpds <atproto id>'")
|
||||||
|
s.isLoading = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer func() { s.isLoading = false }()
|
||||||
|
var pds *models.Pds
|
||||||
|
var err error
|
||||||
|
if s.activePds != nil && string(s.activePds.AtId) == args[1] {
|
||||||
|
pds, err = s.r.ReloadPds(args[1])
|
||||||
|
} else {
|
||||||
|
pds, err = s.r.GetPDS(args[1])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log(err.Error())
|
||||||
|
return
|
||||||
|
} else if pds == nil {
|
||||||
|
s.Log("PDS (%s) Not Found.", args[1])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.doOpen = false
|
||||||
|
slog.Default().Debug(fmt.Sprintf("Retrieved: %s (%s) at %s", pds.AtId, pds.Did, time.Now().Format(time.RFC3339)))
|
||||||
|
s.activePds = pds
|
||||||
|
s.updatePdsListing()
|
||||||
|
n, err := s.pdsListing.GetActiveNode()
|
||||||
|
if err == nil && n != nil {
|
||||||
|
s.changePdsList(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.layout.ActivateWidget(s.columns)
|
||||||
|
s.columns.ActivateWidget(s.pdsListing)
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) cliAuthPds(args ...string) bool {
|
||||||
|
if s.activePds == nil {
|
||||||
|
s.Log("No active PDS.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s.isLoading = true
|
||||||
|
go func() {
|
||||||
|
defer func() { s.isLoading = false }()
|
||||||
|
atid := s.activePds.AtId.String()
|
||||||
|
callbackRes := make(chan url.Values, 1)
|
||||||
|
listenPort, err := s.r.Auth.ListenForCallback(callbackRes)
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error Instantiating HTTP Server for Callback: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Log("Listening on %d", listenPort)
|
||||||
|
var authUrl string
|
||||||
|
authUrl, err = s.r.Auth.StartAuthFlow(listenPort, atid, callbackRes)
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error starting auth flow: %w", err)
|
||||||
|
}
|
||||||
|
<<<<<<< HEAD
|
||||||
|
s.alert.SetTitle("Authentication Started")
|
||||||
|
s.alert.SetMessage(fmt.Sprintf("OAuth Process Started.\nIf a browser window didn't open, you can open this URL manually:\n%s", authUrl))
|
||||||
|
s.alert.SetVisible(true)
|
||||||
|
s.alert.SetActive(true)
|
||||||
|
s.showAlert = true
|
||||||
|
=======
|
||||||
|
s.Log("Authentication Started")
|
||||||
|
s.Log(fmt.Sprintf("OAuth Process Started.\nIf a browser window didn't open, you can open this URL manually:\n%s", strings.ReplaceAll(authUrl, "%", "%%")))
|
||||||
|
>>>>>>> 367d62ff009186e5aa584fd069dd57aaf7d46a8a
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) cliBackupPds(args ...string) bool {
|
||||||
|
if s.activePds == nil {
|
||||||
|
s.Log("No active PDS.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s.isLoading = true
|
||||||
|
go func() {
|
||||||
|
defer func() { s.isLoading = false }()
|
||||||
|
nm, sz, err := s.activePds.Backup()
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Log("Backup Created: %s (%d bytes)", nm, sz)
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) cliSendStatus(args ...string) bool {
|
||||||
|
if s.activePds == nil {
|
||||||
|
s.Log("No active PDS.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s.isLoading = true
|
||||||
|
go func() {
|
||||||
|
defer func() { s.isLoading = false }()
|
||||||
|
if !s.r.Auth.HasAuth(s.activePds.Did) {
|
||||||
|
s.Log("Not authorized. Run `authpds`")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := s.r.SendToPDS(s.activePds.Did)
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error sending status: %w", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) updatePdsListing() {
|
||||||
|
s.pdsListing.SetTitle(fmt.Sprintf("─ %s (%s)", s.activePds.AtId.String(), s.activePds.Did.String()))
|
||||||
|
s.pdsListing.Clear()
|
||||||
|
nsidList := s.activePds.NSIDStringList()
|
||||||
|
var tree []*wd.TreeNode
|
||||||
|
for i, v := range nsidList {
|
||||||
|
t := wd.NewTreeNode(v, v)
|
||||||
|
nsid := s.activePds.NSIDs[i]
|
||||||
|
rIds := s.activePds.GetRecordIdsFor(nsid)
|
||||||
|
for j := range rIds {
|
||||||
|
label := rIds[j]
|
||||||
|
if viper.GetBool(data.KeyRecNmInfer) {
|
||||||
|
if rec, ok := s.activePds.Records[rIds[j]]; ok {
|
||||||
|
for k := range rec {
|
||||||
|
if k == "name" || k == "title" || k == "label" || k == "displayName" {
|
||||||
|
if f, ok := rec[k].(string); ok {
|
||||||
|
label = fmt.Sprintf("%s (%s)", f, rIds[j])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := wd.NewTreeNode(label, rIds[j])
|
||||||
|
t.AddChild(c)
|
||||||
|
}
|
||||||
|
tree = append(tree, t)
|
||||||
|
}
|
||||||
|
s.pdsListing.SetTree(tree)
|
||||||
|
s.layout.ActivateWidget(s.columns)
|
||||||
|
s.columns.ActivateWidget(s.pdsListing)
|
||||||
|
s.pdsListing.SetActive(true)
|
||||||
|
s.pdsListing.SetFocusable(true)
|
||||||
|
s.hideCli()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) selectPdsListingEntry(tn *wd.TreeNode) bool {
|
||||||
|
if !s.updateJsonView(tn) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if tn.HasChildren() {
|
||||||
|
tn.ToggleExpand()
|
||||||
|
s.pdsListing.UpdateList()
|
||||||
|
} else {
|
||||||
|
s.columns.ActivateWidget(s.jsonContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) changePdsList(tn *wd.TreeNode) bool {
|
||||||
|
upd := s.updateJsonView(tn)
|
||||||
|
upd = s.updateStatusPathBlock(tn)
|
||||||
|
return upd
|
||||||
|
}
|
||||||
|
func (s *ScreenHome) updateStatusPathBlock(tn *wd.TreeNode) bool {
|
||||||
|
s.stPathBlock.SetParts(tn.GetLabelPath())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) updateJsonView(tn *wd.TreeNode) bool {
|
||||||
|
// TODO: Update JSON View
|
||||||
|
if tn.Depth() == 0 {
|
||||||
|
nsid, err := syntax.ParseNSID(tn.Value())
|
||||||
|
if err != nil {
|
||||||
|
s.Log("error parsing NSID from %s: %w", tn.Value(), err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
recordIds := s.activePds.GetRecordIdsFor(nsid)
|
||||||
|
s.jsonContent.SetValue(recordIds)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
s.jsonContent.SetValue(s.activePds.Records[tn.Value()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ var (
|
|||||||
Name = "expds"
|
Name = "expds"
|
||||||
cfgFile string
|
cfgFile string
|
||||||
ConfigDir = ""
|
ConfigDir = ""
|
||||||
|
debug = false
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "expds",
|
Use: "expds",
|
||||||
Short: "Utility to edit and view PDS",
|
Short: "Utility to edit and view PDS",
|
||||||
@@ -60,6 +61,7 @@ func Execute() {
|
|||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "debug mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (a *AppLogHandler) addGroups(groups ...string) { a.groups = append(a.group
|
|||||||
// AppLogHandler can handle all levels
|
// AppLogHandler can handle all levels
|
||||||
func (a *AppLogHandler) Enabled(_ context.Context, lvl slog.Level) bool { return lvl >= a.level }
|
func (a *AppLogHandler) Enabled(_ context.Context, lvl slog.Level) bool { return lvl >= a.level }
|
||||||
|
|
||||||
func (a *AppLogHandler) Handle(ctx context.Context, rcd slog.Record) error {
|
func (a *AppLogHandler) Handle(_ context.Context, rcd slog.Record) error {
|
||||||
if a.logFunc == nil {
|
if a.logFunc == nil {
|
||||||
return errors.New("no log func defined")
|
return errors.New("no log func defined")
|
||||||
}
|
}
|
||||||
|
|||||||
28
data/repo.go
28
data/repo.go
@@ -40,6 +40,7 @@ type Repo struct {
|
|||||||
|
|
||||||
handler *AppLogHandler
|
handler *AppLogHandler
|
||||||
logFunc func(string, ...any)
|
logFunc func(string, ...any)
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
context context.Context
|
context context.Context
|
||||||
}
|
}
|
||||||
@@ -56,6 +57,7 @@ func NewRepo() (*Repo, error) {
|
|||||||
} else {
|
} else {
|
||||||
r.handler.SetLevel(slog.LevelWarn)
|
r.handler.SetLevel(slog.LevelWarn)
|
||||||
}
|
}
|
||||||
|
r.Logger = slog.Default()
|
||||||
var err error
|
var err error
|
||||||
r.Auth, err = NewAuthRepo(r)
|
r.Auth, err = NewAuthRepo(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,10 +66,7 @@ func NewRepo() (*Repo, error) {
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
|
func (r *Repo) fetchPds(atId string) (*models.Pds, error) {
|
||||||
if p, ok := r.LoadedPDSs[atId]; ok && time.Since(p.RefreshTime) < r.BestBy {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
p, err := models.NewPdsFromDid(atId)
|
p, err := models.NewPdsFromDid(atId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -76,8 +75,19 @@ func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) SendToPDS() error {
|
func (r *Repo) ReloadPds(atId string) (*models.Pds, error) {
|
||||||
session, err := r.Auth.GetSession()
|
return r.fetchPds(atId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
|
||||||
|
if p, ok := r.LoadedPDSs[atId]; ok && time.Since(p.RefreshTime) < r.BestBy {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
return r.fetchPds(atId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) SendToPDS(did syntax.DID) error {
|
||||||
|
session, err := r.Auth.GetSession(did)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -94,11 +104,11 @@ func (r *Repo) SendToPDS() error {
|
|||||||
var resp struct {
|
var resp struct {
|
||||||
Uri syntax.ATURI `json:"uri"`
|
Uri syntax.ATURI `json:"uri"`
|
||||||
}
|
}
|
||||||
slog.Debug("posting expds status...")
|
r.Logger.Debug("posting expds status...")
|
||||||
if err := c.Post(r.context, "com.atproto.repo.CreateRecord", body, &resp); err != nil {
|
if err := c.Post(r.context, "com.atproto.repo.CreateRecord", body, &resp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
slog.Debug(fmt.Sprintf("posted: %s :: %s", resp.Uri.Authority(), resp.Uri.RecordKey()))
|
r.Logger.Debug(fmt.Sprintf("posted: %s :: %s", resp.Uri.Authority(), resp.Uri.RecordKey()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,4 +116,6 @@ func (r *Repo) SetLogFunc(l func(string, ...any)) {
|
|||||||
r.logFunc = l
|
r.logFunc = l
|
||||||
r.handler = NewAppLogHandler(r.logFunc)
|
r.handler = NewAppLogHandler(r.logFunc)
|
||||||
slog.SetDefault(slog.New(r.handler))
|
slog.SetDefault(slog.New(r.handler))
|
||||||
|
r.Logger = slog.Default()
|
||||||
|
r.Logger.Debug("New Log Func Set for slog")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||||
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,12 +27,15 @@ type AuthRepo struct {
|
|||||||
|
|
||||||
session *oauth.ClientSessionData
|
session *oauth.ClientSessionData
|
||||||
authError error
|
authError error
|
||||||
|
|
||||||
|
Logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthRepo(r *Repo) (*AuthRepo, error) {
|
func NewAuthRepo(r *Repo) (*AuthRepo, error) {
|
||||||
a := &AuthRepo{
|
a := &AuthRepo{
|
||||||
r: r,
|
r: r,
|
||||||
context: r.context,
|
context: r.context,
|
||||||
|
Logger: r.Logger,
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
a.oauthConfig, a.oauthClient, a.store, err = a.buildOAuthClient()
|
a.oauthConfig, a.oauthClient, a.store, err = a.buildOAuthClient()
|
||||||
@@ -109,20 +114,22 @@ func (r *AuthRepo) ListenForCallback(res chan url.Values) (int, error) {
|
|||||||
if !errors.Is(err, http.ErrServerClosed) {
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
r.Logger.Debug("Server Shut Down")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return listener.Addr().(*net.TCPAddr).Port, nil
|
return listener.Addr().(*net.TCPAddr).Port, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AuthRepo) HasAuth() bool {
|
func (r *AuthRepo) HasAuth(did syntax.DID) bool {
|
||||||
sess, err := r.store.GetMostRecentSession(r.context)
|
sess, err := r.GetSession(did)
|
||||||
return err != nil || sess == nil
|
return err == nil && sess != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AuthRepo) GetSession() (*oauth.ClientSession, error) {
|
func (r *AuthRepo) GetSession(did syntax.DID) (*oauth.ClientSession, error) {
|
||||||
sess, err := r.store.GetMostRecentSession(r.context)
|
sess, err := r.store.GetMostRecentSessionFor(r.context, did)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting most recent session: %w", err)
|
return nil, fmt.Errorf("error getting most recent session: %w", err)
|
||||||
}
|
}
|
||||||
|
r.Logger.Warn(fmt.Sprintf("GetSession(): Resuming Session: %s (%s)", sess.SessionID, sess.AccountDID))
|
||||||
return r.oauthClient.ResumeSession(r.context, sess.AccountDID, sess.SessionID)
|
return r.oauthClient.ResumeSession(r.context, sess.AccountDID, sess.SessionID)
|
||||||
}
|
}
|
||||||
|
|||||||
139
data/repo_auth.go.orig
Normal file
139
data/repo_auth.go.orig
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||||
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthRepo struct {
|
||||||
|
r *Repo
|
||||||
|
oauthClient *oauth.ClientApp
|
||||||
|
oauthConfig *oauth.ClientConfig
|
||||||
|
store *SqliteStore
|
||||||
|
context context.Context
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
session *oauth.ClientSessionData
|
||||||
|
authError error
|
||||||
|
=======
|
||||||
|
session *oauth.ClientSessionData
|
||||||
|
|
||||||
|
Logger *slog.Logger
|
||||||
|
>>>>>>> 367d62ff009186e5aa584fd069dd57aaf7d46a8a
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthRepo(r *Repo) (*AuthRepo, error) {
|
||||||
|
a := &AuthRepo{
|
||||||
|
r: r,
|
||||||
|
context: r.context,
|
||||||
|
Logger: r.Logger,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
a.oauthConfig, a.oauthClient, a.store, err = a.buildOAuthClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the OAuthClient connected to our sqlite db
|
||||||
|
func (r *AuthRepo) buildOAuthClient() (*oauth.ClientConfig, *oauth.ClientApp, *SqliteStore, error) {
|
||||||
|
config := oauth.ClientConfig{
|
||||||
|
ClientID: "https://expds.bullercodeworks.com/oauth-client-metadata.json",
|
||||||
|
Scopes: []string{"atproto", "repo:*", "blob:*/*"},
|
||||||
|
UserAgent: "expds",
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := NewSqliteStore(&SqliteStoreConfig{
|
||||||
|
DatabasePath: r.prepareDbPath(),
|
||||||
|
SessionExpiryDuration: time.Hour * 24 * 90,
|
||||||
|
SessionInactivityDuration: time.Hour * 24 * 14,
|
||||||
|
AuthRequestExpiryDuration: time.Minute * 30,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthClient := oauth.NewClientApp(&config, store)
|
||||||
|
return &config, oauthClient, store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AuthRepo) StartAuthFlow(port int, identifier string, callbackRes chan url.Values) (string, error) {
|
||||||
|
r.oauthConfig.CallbackURL = fmt.Sprintf("http://127.0.0.1:%d/callback", port)
|
||||||
|
authUrl, err := r.oauthClient.StartAuthFlow(r.context, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error logging in: %w", err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(authUrl, "https://") {
|
||||||
|
return "", fmt.Errorf("non-https authUrl")
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
exec.Command("xdg-open", authUrl).Start()
|
||||||
|
r.session, r.authError = r.oauthClient.ProcessCallback(r.context, <-callbackRes)
|
||||||
|
}()
|
||||||
|
return authUrl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follows XDG conventions and creates the directories if necessary.
|
||||||
|
// By default, on linux, this will be "~/.local/share/go-oauth-cli-app/oauth_sessions.sqlite3"
|
||||||
|
func (r *AuthRepo) prepareDbPath() string {
|
||||||
|
return filepath.Join(viper.GetString(KeyDataDir), "expds.sqlite3")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP Server listening for OAuth Response
|
||||||
|
func (r *AuthRepo) ListenForCallback(res chan url.Values) (int, error) {
|
||||||
|
listener, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := &http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.HandleFunc("/callback", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
res <- req.URL.Query()
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte("<!DOCTYPE html><html><body><h2>expds</h2><p>You can safely close this window and return to your application.</p></body></html>\n"))
|
||||||
|
go server.Shutdown(r.context)
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := server.Serve(listener)
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
r.Logger.Debug("Server Shut Down")
|
||||||
|
}()
|
||||||
|
|
||||||
|
return listener.Addr().(*net.TCPAddr).Port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AuthRepo) HasAuth(did syntax.DID) bool {
|
||||||
|
sess, err := r.GetSession(did)
|
||||||
|
return err == nil && sess != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AuthRepo) GetSession(did syntax.DID) (*oauth.ClientSession, error) {
|
||||||
|
sess, err := r.store.GetMostRecentSessionFor(r.context, did)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting most recent session: %w", err)
|
||||||
|
}
|
||||||
|
r.Logger.Warn(fmt.Sprintf("GetSession(): Resuming Session: %s (%s)", sess.SessionID, sess.AccountDID))
|
||||||
|
return r.oauthClient.ResumeSession(r.context, sess.AccountDID, sess.SessionID)
|
||||||
|
}
|
||||||
@@ -96,6 +96,20 @@ func (m *SqliteStore) GetSession(ctx context.Context, did syntax.DID, sessionID
|
|||||||
return &row.Data, nil
|
return &row.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *SqliteStore) GetMostRecentSessionFor(ctx context.Context, did syntax.DID) (*oauth.ClientSessionData, error) {
|
||||||
|
var row storedSessionData
|
||||||
|
res := m.db.WithContext(ctx).Where(&storedSessionData{
|
||||||
|
AccountDid: did,
|
||||||
|
}).Order(clause.OrderByColumn{
|
||||||
|
Column: clause.Column{Name: "updated_at"},
|
||||||
|
Desc: true,
|
||||||
|
}).First(&row)
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
return &row.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
// not part of the ClientAuthStore interface, just used for the CLI app
|
// not part of the ClientAuthStore interface, just used for the CLI app
|
||||||
func (m *SqliteStore) GetMostRecentSession(ctx context.Context) (*oauth.ClientSessionData, error) {
|
func (m *SqliteStore) GetMostRecentSession(ctx context.Context) (*oauth.ClientSessionData, error) {
|
||||||
var row storedSessionData
|
var row storedSessionData
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func (b *StatusBlock) DrawPath(screen tcell.Screen) {
|
|||||||
x := b.x
|
x := b.x
|
||||||
wh.DrawText(x, b.y, b.end, b.style, screen)
|
wh.DrawText(x, b.y, b.end, b.style, screen)
|
||||||
x++
|
x++
|
||||||
pts := fmt.Sprintf(" %s ", strings.Join(b.parts, b.sep))
|
pts := fmt.Sprintf(" %s ", strings.Join(b.parts, b.sep))
|
||||||
wh.DrawText(x, b.y, pts, b.style, screen)
|
wh.DrawText(x, b.y, pts, b.style, screen)
|
||||||
x += len(pts) - len(b.parts)
|
x += len(pts) - len(b.parts)
|
||||||
wh.DrawText(x, b.y, b.end, b.style.Reverse(true), screen)
|
wh.DrawText(x, b.y, b.end, b.style.Reverse(true), screen)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
h "git.bullercodeworks.com/brian/expds/helpers"
|
h "git.bullercodeworks.com/brian/expds/helpers"
|
||||||
t "git.bullercodeworks.com/brian/tcell-widgets"
|
t "git.bullercodeworks.com/brian/tcell-widgets"
|
||||||
@@ -36,7 +37,12 @@ type TreeBrowser struct {
|
|||||||
keyMap *t.KeyMap
|
keyMap *t.KeyMap
|
||||||
vimMode bool
|
vimMode bool
|
||||||
|
|
||||||
|
searching bool
|
||||||
|
searchStr string
|
||||||
|
|
||||||
logger func(string, ...any)
|
logger func(string, ...any)
|
||||||
|
|
||||||
|
m sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ t.Widget = (*TreeBrowser)(nil)
|
var _ t.Widget = (*TreeBrowser)(nil)
|
||||||
@@ -55,6 +61,10 @@ func (w *TreeBrowser) Init(id string, style tcell.Style) {
|
|||||||
t.NewKey(t.BuildEK(tcell.KeyUp), func(_ *tcell.EventKey) bool { return w.MoveUp() }),
|
t.NewKey(t.BuildEK(tcell.KeyUp), func(_ *tcell.EventKey) bool { return w.MoveUp() }),
|
||||||
t.NewKey(t.BuildEK(tcell.KeyDown), func(_ *tcell.EventKey) bool { return w.MoveDown() }),
|
t.NewKey(t.BuildEK(tcell.KeyDown), func(_ *tcell.EventKey) bool { return w.MoveDown() }),
|
||||||
t.NewKey(t.BuildEK(tcell.KeyEnter), func(ev *tcell.EventKey) bool {
|
t.NewKey(t.BuildEK(tcell.KeyEnter), func(ev *tcell.EventKey) bool {
|
||||||
|
if w.searching {
|
||||||
|
w.searching = !w.searching
|
||||||
|
return true
|
||||||
|
}
|
||||||
if w.onSelect != nil {
|
if w.onSelect != nil {
|
||||||
n, err := w.GetActiveNode()
|
n, err := w.GetActiveNode()
|
||||||
if err != nil || n == nil {
|
if err != nil || n == nil {
|
||||||
@@ -97,6 +107,14 @@ func (w *TreeBrowser) Init(id string, style tcell.Style) {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}),
|
}),
|
||||||
|
t.NewKey(t.BuildEKr('/'), func(ev *tcell.EventKey) bool {
|
||||||
|
if !w.searching {
|
||||||
|
w.searching = true
|
||||||
|
w.searchStr = ""
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,12 +129,20 @@ func (w *TreeBrowser) HandleKey(ev *tcell.EventKey) bool {
|
|||||||
if !w.active || !w.focusable {
|
if !w.active || !w.focusable {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return w.keyMap.Handle(ev)
|
if w.keyMap.Handle(ev) {
|
||||||
|
return true
|
||||||
|
} else if w.searching {
|
||||||
|
w.updateSearch(ev)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TreeBrowser) HandleTime(ev *tcell.EventTime) {}
|
func (w *TreeBrowser) HandleTime(ev *tcell.EventTime) {}
|
||||||
|
|
||||||
func (w *TreeBrowser) Draw(screen tcell.Screen) {
|
func (w *TreeBrowser) Draw(screen tcell.Screen) {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
if !w.visible {
|
if !w.visible {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -170,9 +196,23 @@ func (w *TreeBrowser) Draw(screen tcell.Screen) {
|
|||||||
if len(txt) > w.w-brdSz && w.w-brdSz >= 0 {
|
if len(txt) > w.w-brdSz && w.w-brdSz >= 0 {
|
||||||
txt = txt[:(w.w - brdSz)]
|
txt = txt[:(w.w - brdSz)]
|
||||||
}
|
}
|
||||||
wh.DrawText(x, y, txt, dS.Reverse(rev), screen)
|
if w.searching && strings.Contains(txt, w.searchStr) {
|
||||||
|
// TODO: Fix multi-byte depth indicator
|
||||||
|
srchIdx := strings.Index(txt, w.searchStr)
|
||||||
|
endSrchIdx := srchIdx + len(w.searchStr)
|
||||||
|
wh.DrawText(x, y, txt[:srchIdx], dS.Reverse(rev), screen)
|
||||||
|
wh.DrawText(x+srchIdx, y, txt[srchIdx:endSrchIdx], dS.Reverse(!rev), screen)
|
||||||
|
if len(txt) > endSrchIdx {
|
||||||
|
wh.DrawText(x+endSrchIdx, y, txt[endSrchIdx:], dS.Reverse(rev), screen)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wh.DrawText(x, y, txt, dS.Reverse(rev), screen)
|
||||||
|
}
|
||||||
y += 1
|
y += 1
|
||||||
}
|
}
|
||||||
|
if w.searching {
|
||||||
|
wh.DrawText(w.x, w.y+w.h, fmt.Sprintf("Searching: %s", w.searchStr), dS, screen)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TreeBrowser) SetStyle(s tcell.Style) { w.style = s }
|
func (w *TreeBrowser) SetStyle(s tcell.Style) { w.style = s }
|
||||||
@@ -197,6 +237,8 @@ func (w *TreeBrowser) SetW(wd int) { w.SetSize(t.Coord{X: wd, Y: w.h}) }
|
|||||||
func (w *TreeBrowser) SetH(h int) { w.SetSize(t.Coord{X: w.w, Y: h}) }
|
func (w *TreeBrowser) SetH(h int) { w.SetSize(t.Coord{X: w.w, Y: h}) }
|
||||||
func (w *TreeBrowser) SetSize(c t.Coord) { w.w, w.h = c.X, c.Y }
|
func (w *TreeBrowser) SetSize(c t.Coord) { w.w, w.h = c.X, c.Y }
|
||||||
func (w *TreeBrowser) WantW() int {
|
func (w *TreeBrowser) WantW() int {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
var want int
|
var want int
|
||||||
for i := range w.list {
|
for i := range w.list {
|
||||||
want = h.MaxI(want, len(w.list[i]))
|
want = h.MaxI(want, len(w.list[i]))
|
||||||
@@ -204,6 +246,8 @@ func (w *TreeBrowser) WantW() int {
|
|||||||
return w.w
|
return w.w
|
||||||
}
|
}
|
||||||
func (w *TreeBrowser) WantH() int {
|
func (w *TreeBrowser) WantH() int {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
want := len(w.list)
|
want := len(w.list)
|
||||||
if len(w.border) > 0 {
|
if len(w.border) > 0 {
|
||||||
return want + 2
|
return want + 2
|
||||||
@@ -225,8 +269,8 @@ func (w *TreeBrowser) ClearBorder() { w.border = []rune{}
|
|||||||
func (w *TreeBrowser) SetOnChange(c func(*TreeNode) bool) { w.onChange = c }
|
func (w *TreeBrowser) SetOnChange(c func(*TreeNode) bool) { w.onChange = c }
|
||||||
func (w *TreeBrowser) SetOnSelect(s func(*TreeNode) bool) { w.onSelect = s }
|
func (w *TreeBrowser) SetOnSelect(s func(*TreeNode) bool) { w.onSelect = s }
|
||||||
func (w *TreeBrowser) SetVimMode(b bool) { w.vimMode = b }
|
func (w *TreeBrowser) SetVimMode(b bool) { w.vimMode = b }
|
||||||
func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) {
|
func (w *TreeBrowser) nmGetActiveNode() (*TreeNode, error) {
|
||||||
if len(w.listNodes) < 0 {
|
if len(w.listNodes) <= 0 {
|
||||||
return nil, errors.New("no nodes")
|
return nil, errors.New("no nodes")
|
||||||
}
|
}
|
||||||
if w.cursor < 0 {
|
if w.cursor < 0 {
|
||||||
@@ -237,6 +281,11 @@ func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) {
|
|||||||
}
|
}
|
||||||
return w.listNodes[w.cursor], nil
|
return w.listNodes[w.cursor], nil
|
||||||
}
|
}
|
||||||
|
func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
|
return w.nmGetActiveNode()
|
||||||
|
}
|
||||||
func (w *TreeBrowser) SetCursorWrap(b bool) { w.cursorWrap = b }
|
func (w *TreeBrowser) SetCursorWrap(b bool) { w.cursorWrap = b }
|
||||||
func (w *TreeBrowser) MoveUp() bool {
|
func (w *TreeBrowser) MoveUp() bool {
|
||||||
if w.cursor > 0 {
|
if w.cursor > 0 {
|
||||||
@@ -305,22 +354,30 @@ func (w *TreeBrowser) PageDn() bool {
|
|||||||
func (w *TreeBrowser) Title() string { return w.title }
|
func (w *TreeBrowser) Title() string { return w.title }
|
||||||
func (w *TreeBrowser) SetTitle(ttl string) { w.title = ttl }
|
func (w *TreeBrowser) SetTitle(ttl string) { w.title = ttl }
|
||||||
func (w *TreeBrowser) SetTree(l []*TreeNode) {
|
func (w *TreeBrowser) SetTree(l []*TreeNode) {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
w.nodes = l
|
w.nodes = l
|
||||||
w.UpdateList()
|
w.nmUpdateList()
|
||||||
}
|
}
|
||||||
func (w *TreeBrowser) Clear() {
|
func (w *TreeBrowser) Clear() {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
w.nodes = []*TreeNode{}
|
w.nodes = []*TreeNode{}
|
||||||
w.UpdateList()
|
w.nmUpdateList()
|
||||||
}
|
}
|
||||||
func (w *TreeBrowser) Add(n *TreeNode) {
|
func (w *TreeBrowser) Add(n *TreeNode) {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Lock()
|
||||||
if n.depthIndic == "" {
|
if n.depthIndic == "" {
|
||||||
n.depthIndic = w.depthIndic
|
n.depthIndic = w.depthIndic
|
||||||
}
|
}
|
||||||
w.nodes = append(w.nodes, n)
|
w.nodes = append(w.nodes, n)
|
||||||
w.UpdateList()
|
w.nmUpdateList()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TreeBrowser) UpdateList() {
|
// Update the list, intended to be called locally within other functions that
|
||||||
|
// handle the mutex
|
||||||
|
func (w *TreeBrowser) nmUpdateList() {
|
||||||
w.list = []string{}
|
w.list = []string{}
|
||||||
w.listNodes = []*TreeNode{}
|
w.listNodes = []*TreeNode{}
|
||||||
for i := range w.nodes {
|
for i := range w.nodes {
|
||||||
@@ -335,6 +392,98 @@ func (w *TreeBrowser) UpdateList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *TreeBrowser) UpdateList() {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
|
w.nmUpdateList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TreeBrowser) nmSetNodeActive(tn *TreeNode) {
|
||||||
|
// Make sure that the selected node is visible
|
||||||
|
wrk := tn.parent
|
||||||
|
for wrk != nil {
|
||||||
|
wrk.expanded = true
|
||||||
|
wrk = wrk.parent
|
||||||
|
}
|
||||||
|
w.nmUpdateList()
|
||||||
|
for i := range w.listNodes {
|
||||||
|
if w.listNodes[i] == tn {
|
||||||
|
w.cursor = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (w *TreeBrowser) SetNodeActive(tn *TreeNode) {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
|
w.nmSetNodeActive(tn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TreeBrowser) updateSearch(ev *tcell.EventKey) {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
|
|
||||||
|
if len(w.nodes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if wh.IsBS(*ev) {
|
||||||
|
if len(w.searchStr) > 0 {
|
||||||
|
w.searchStr = w.searchStr[:len(w.searchStr)-1]
|
||||||
|
if len(w.searchStr) == 0 {
|
||||||
|
w.searching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.searchStr = fmt.Sprintf("%s%s", w.searchStr, string(ev.Rune()))
|
||||||
|
wrk, _ := w.nmGetActiveNode()
|
||||||
|
if wrk == nil {
|
||||||
|
wrk = w.nodes[0]
|
||||||
|
}
|
||||||
|
// Check the ative node & it's children for the search
|
||||||
|
if fnd := wrk.SearchLabels(w.searchStr); fnd != nil {
|
||||||
|
w.nmSetNodeActive(fnd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Didn't find a child of the active node that matched, look for a sibling
|
||||||
|
if wrk.parent != nil {
|
||||||
|
if fnd := wrk.parent.SearchLabels(w.searchStr); fnd != nil {
|
||||||
|
w.nmSetNodeActive(fnd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check the next browser node
|
||||||
|
var stIdx int
|
||||||
|
for i := range w.nodes {
|
||||||
|
if w.nodes[i] == wrk {
|
||||||
|
stIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i < len(w.nodes); i++ {
|
||||||
|
idx := (i + stIdx) % len(w.nodes)
|
||||||
|
if fnd := w.nodes[idx].SearchLabels(w.searchStr); fnd != nil {
|
||||||
|
w.nmSetNodeActive(fnd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TreeBrowser) getCurrentLine() string {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
|
l := len(w.list)
|
||||||
|
if l == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if w.cursor < 0 || w.cursor >= l {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return w.list[w.cursor]
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Tree Node
|
* Tree Node
|
||||||
*/
|
*/
|
||||||
@@ -370,6 +519,7 @@ func (tn *TreeNode) GetLabelPath() []string {
|
|||||||
}
|
}
|
||||||
return append(path, tn.Label())
|
return append(path, tn.Label())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tn *TreeNode) getList() []string {
|
func (tn *TreeNode) getList() []string {
|
||||||
pre := strings.Repeat(tn.depthIndic, tn.Depth())
|
pre := strings.Repeat(tn.depthIndic, tn.Depth())
|
||||||
ret := []string{fmt.Sprintf("%s%s", pre, tn.label)}
|
ret := []string{fmt.Sprintf("%s%s", pre, tn.label)}
|
||||||
@@ -380,6 +530,7 @@ func (tn *TreeNode) getList() []string {
|
|||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tn *TreeNode) getVisibleNodeList() []*TreeNode {
|
func (tn *TreeNode) getVisibleNodeList() []*TreeNode {
|
||||||
ret := []*TreeNode{tn}
|
ret := []*TreeNode{tn}
|
||||||
if tn.expanded {
|
if tn.expanded {
|
||||||
@@ -390,6 +541,19 @@ func (tn *TreeNode) getVisibleNodeList() []*TreeNode {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tn *TreeNode) SearchLabels(f string) *TreeNode {
|
||||||
|
if strings.Contains(tn.label, f) {
|
||||||
|
return tn
|
||||||
|
}
|
||||||
|
for i := 0; i < len(tn.children); i++ {
|
||||||
|
fnd := tn.children[i].SearchLabels(f)
|
||||||
|
if fnd != nil {
|
||||||
|
return fnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (tn *TreeNode) ToggleExpand() { tn.expanded = !tn.expanded }
|
func (tn *TreeNode) ToggleExpand() { tn.expanded = !tn.expanded }
|
||||||
|
|
||||||
func (tn *TreeNode) AddChild(t *TreeNode, rest ...*TreeNode) {
|
func (tn *TreeNode) AddChild(t *TreeNode, rest ...*TreeNode) {
|
||||||
@@ -415,3 +579,44 @@ func (tn *TreeNode) GetPath() []string {
|
|||||||
}
|
}
|
||||||
return append(path, tn.value)
|
return append(path, tn.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tn *TreeNode) GetChildren() []*TreeNode { return tn.children }
|
||||||
|
|
||||||
|
func (tn *TreeNode) GetFirstChild() *TreeNode {
|
||||||
|
if !tn.HasChildren() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tn.children[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tn *TreeNode) GetPrevChild(before *TreeNode) *TreeNode {
|
||||||
|
if !tn.HasChildren() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for i := len(tn.children) - 1; i >= 0; i-- {
|
||||||
|
if found {
|
||||||
|
return tn.children[i]
|
||||||
|
}
|
||||||
|
if tn.children[i] == before {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tn *TreeNode) GetNextChild(after *TreeNode) *TreeNode {
|
||||||
|
if !tn.HasChildren() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for i := range tn.children {
|
||||||
|
if found {
|
||||||
|
return tn.children[i]
|
||||||
|
}
|
||||||
|
if tn.children[i] == after {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user