From dd2a06c1e1391fe6242015330b7c61fa37fd67cc Mon Sep 17 00:00:00 2001 From: tjp Date: Wed, 17 Jan 2024 08:55:42 -0700 Subject: [PATCH] start of a bubbletea TUI --- actions.go | 148 +++++++++++++++++++++++++++++++++------------------- command.go | 30 +++++------ files.go | 16 +++--- go.mod | 17 +++++- go.sum | 36 +++++++++++++ handlers.go | 15 +++--- identity.go | 12 ++--- main.go | 108 +++++++++++++++++++++++--------------- mark.go | 8 +-- state.go | 11 +--- tour.go | 20 +++---- tui.go | 43 +++++++++++++++ 12 files changed, 310 insertions(+), 154 deletions(-) create mode 100644 tui.go diff --git a/actions.go b/actions.go index c91e479..c11e677 100644 --- a/actions.go +++ b/actions.go @@ -32,13 +32,22 @@ var ( ErrCantMoveRelative = errors.New("next/previous only work after navigating to a link on a page") ErrAlreadyAtTop = errors.New("already at the site root") ErrInvalidNumericLink = errors.New("no link with that number") - ErrInvalidLink = errors.New("that doesn't look like a valid URL") ErrSaveNeedsFilename = errors.New("save requires a filename argument") ErrInvalidMarkArgs = errors.New("mark what?") ErrInvalidTourArgs = errors.New("tour what?") ErrOnlyTextGemini = errors.New("that is only supported for text/gemini pages") ) +func ErrInvalidLink(invalidURL string) error { + return invalidLinkErr(invalidURL) +} + +type invalidLinkErr string + +func (ie invalidLinkErr) Error() string { + return fmt.Sprintf("that doesn't look like a valid URL: %s", string(ie)) +} + func About(_ *BrowserState) error { _, err := fmt.Println(` ... @@ -73,13 +82,13 @@ It was written by TJP and released to the public domain. return err } -func Navigate(state *BrowserState, target *url.URL, navIndex int, conf *Config) error { +func Navigate(state *BrowserState, target *url.URL, navIndex int) error { if state.Url == nil || target.String() != state.Url.String() { pushHistory(state, target, navIndex) } state.Modal = nil - return Reload(state, conf) + return Reload(state) } func pushHistory(state *BrowserState, target *url.URL, navIndex int) { @@ -91,6 +100,25 @@ func pushHistory(state *BrowserState, target *url.URL, navIndex int) { NavIndex: navIndex, } hist.Forward = state.History + + purgeOldHistory(state) +} + +func purgeOldHistory(state *BrowserState) { + if state.Depth <= state.SavedHistoryDepth { + return + } + + d := state.SavedHistoryDepth + h := state.History + for d > 0 { + h = h.Back + d -= 1 + } + + h.Body = nil + h.Formatted = "" + h.Links = nil } func gopherURL(u *url.URL) (string, sliderule.Status) { @@ -103,7 +131,7 @@ func gopherURL(u *url.URL) (string, sliderule.Status) { return clone.String(), sliderule.Status(itemType) } -func Reload(state *BrowserState, conf *Config) error { +func Reload(state *BrowserState) error { if state.Url == nil { return ErrMustBeOnAPage } @@ -144,9 +172,9 @@ func Reload(state *BrowserState, conf *Config) error { } } + if state.Url.Scheme == "gemini" { outer: - for { - if state.Url.Scheme == "gemini" { + for { switch response.Status { case gemini.StatusInput: state.Readline.SetPrompt(response.Meta.(string) + " ") @@ -178,23 +206,33 @@ outer: default: return fmt.Errorf("gemini response %s: %s", gemini.StatusName(response.Status), response.Meta.(string)) } - } else { - break } } state.DocType = docType(state.Url, response) + state.Url = returnedURL(state.Url, response) state.Body, err = io.ReadAll(response.Body) if err != nil { return err } - state.Formatted, state.Links, err = parseDoc(state.DocType, state.Body, conf) + state.Formatted, state.Links, err = parseDoc(state.DocType, state.Body, state.Config) if err != nil { return err } - return HandleResource(state, conf) + return HandleResource(state) +} + +func returnedURL(requested *url.URL, response *sliderule.Response) *url.URL { + _, gopherType := gopherURL(requested) + if gopherType == 0 { + return response.Request.URL + } + + u := *response.Request.URL + u.Path = "/" + string([]byte{byte(gopherType)}) + u.Path + return &u } func requestCtx(timeout time.Duration) (context.Context, context.CancelFunc) { @@ -206,7 +244,7 @@ func requestCtx(timeout time.Duration) (context.Context, context.CancelFunc) { } func fetch(state *BrowserState, u string, tlsConf *tls.Config) (*sliderule.Response, error) { - ctx, cancel := requestCtx(state.Timeout) + ctx, cancel := requestCtx(state.Timeout.Duration) defer cancel() tlsConf.ClientSessionCache = nil @@ -229,7 +267,7 @@ func fetch(state *BrowserState, u string, tlsConf *tls.Config) (*sliderule.Respo return nil, err } - ctx, cancel = requestCtx(state.Timeout) + ctx, cancel = requestCtx(state.Timeout.Duration) defer cancel() return sliderule.NewClient(tlsConf).Fetch(ctx, u) } else if err != nil { @@ -239,7 +277,7 @@ func fetch(state *BrowserState, u string, tlsConf *tls.Config) (*sliderule.Respo } func upload(state *BrowserState, u string, body io.Reader, tlsConf *tls.Config) (*sliderule.Response, error) { - ctx, cancel := requestCtx(state.Timeout) + ctx, cancel := requestCtx(state.Timeout.Duration) defer cancel() tlsConf.ClientSessionCache = nil @@ -261,7 +299,7 @@ func upload(state *BrowserState, u string, body io.Reader, tlsConf *tls.Config) return nil, err } - ctx, cancel = requestCtx(state.Timeout) + ctx, cancel = requestCtx(state.Timeout.Duration) defer cancel() return sliderule.NewClient(tlsConf).Upload(ctx, u, body) } else if err != nil { @@ -327,21 +365,25 @@ func back(state *BrowserState) error { if state.Back == nil { return ErrNoPreviousHistory } - state.History = state.Back state.Modal = nil + state.History = state.Back + + if state.Body == nil { + return Reload(state) + } return nil } -func Back(state *BrowserState, conf *Config, num int) error { +func Back(state *BrowserState, num int) error { for i := 0; i < num; i += 1 { if err := back(state); err != nil { return err } } - return HandleResource(state, conf) + return HandleResource(state) } -func Forward(state *BrowserState, conf *Config, num int) error { +func Forward(state *BrowserState, num int) error { for i := 0; i < num; i += 1 { if state.Forward == nil { return ErrNoNextHistory @@ -350,10 +392,10 @@ func Forward(state *BrowserState, conf *Config, num int) error { } state.Modal = nil - return HandleResource(state, conf) + return HandleResource(state) } -func Next(state *BrowserState, conf *Config) error { +func Next(state *BrowserState) error { switch state.NavIndex { case -1: return ErrCantMoveRelative @@ -369,10 +411,10 @@ func Next(state *BrowserState, conf *Config) error { u := state.Url.ResolveReference(state.Links[index].Target) - return Navigate(state, u, index, conf) + return Navigate(state, u, index) } -func Previous(state *BrowserState, conf *Config) error { +func Previous(state *BrowserState) error { switch state.NavIndex { case -1: return ErrCantMoveRelative @@ -388,10 +430,10 @@ func Previous(state *BrowserState, conf *Config) error { u := state.Url.ResolveReference(state.Links[index].Target) - return Navigate(state, u, index, conf) + return Navigate(state, u, index) } -func Root(state *BrowserState, tilde bool, conf *Config) error { +func Root(state *BrowserState, tilde bool) error { if state.Url == nil { return ErrMustBeOnAPage } @@ -411,10 +453,10 @@ func Root(state *BrowserState, tilde bool, conf *Config) error { u.Path = base } - return Navigate(state, &u, -1, conf) + return Navigate(state, &u, -1) } -func Up(state *BrowserState, conf *Config) error { +func Up(state *BrowserState) error { if state.Url == nil { return ErrMustBeOnAPage } @@ -429,16 +471,16 @@ func Up(state *BrowserState, conf *Config) error { u.RawQuery = "" u.Fragment = "" - return Navigate(state, &u, -1, conf) + return Navigate(state, &u, -1) } -func Go(state *BrowserState, dest string, conf *Config) error { - u, idx, err := parseURL(dest, state, conf.DefaultScheme) +func Go(state *BrowserState, dest string) error { + u, idx, err := parseURL(dest, state, state.DefaultScheme) if err != nil { return err } - return Navigate(state, u, idx, conf) + return Navigate(state, u, idx) } func parseURL(str string, state *BrowserState, defaultScheme string) (*url.URL, int, error) { @@ -458,7 +500,7 @@ func parseURL(str string, state *BrowserState, defaultScheme string) (*url.URL, } else if strings.HasPrefix(str, "t:") { i, err := strconv.Atoi(str[2:]) if err != nil { - return nil, -1, ErrInvalidLink + return nil, -1, ErrInvalidLink(str) } if i < 0 || i >= len(state.CurrentTour.Links) { @@ -468,10 +510,10 @@ func parseURL(str string, state *BrowserState, defaultScheme string) (*url.URL, } else if strings.HasPrefix(str, "t[") { idx := strings.IndexByte(str, ']') if idx < 0 || idx >= len(str)-2 || str[idx+1] != ':' { - return nil, -1, ErrInvalidLink + return nil, -1, ErrInvalidLink(str) } if i, err := strconv.Atoi(str[idx+2:]); err != nil { - return nil, -1, ErrInvalidLink + return nil, -1, ErrInvalidLink(str) } else { _, tour, err := findTour(state, str[2:idx]) if err != nil { @@ -499,7 +541,7 @@ func parseURL(str string, state *BrowserState, defaultScheme string) (*url.URL, i = -1 u, err = url.Parse(str) if err != nil { - return nil, -1, ErrInvalidLink + return nil, -1, ErrInvalidLink(str) } if u.Scheme == "" { u.Scheme = defaultScheme @@ -510,7 +552,7 @@ func parseURL(str string, state *BrowserState, defaultScheme string) (*url.URL, } if u.Hostname() == "" { - return nil, -1, ErrInvalidLink + return nil, -1, ErrInvalidLink(u.String()) } return u, i, nil @@ -563,12 +605,12 @@ func Print(state *BrowserState) error { return print(state) } -func HandleResource(state *BrowserState, conf *Config) error { +func HandleResource(state *BrowserState) error { if state.Modal != nil { return Print(state) } - if handler, ok := conf.Handlers[state.DocType]; ok { + if handler, ok := state.Handlers[state.DocType]; ok { return Pipe(state, handler) } @@ -577,10 +619,10 @@ func HandleResource(state *BrowserState, conf *Config) error { return print(state) } - return Save(state, path.Base(state.Url.Path), conf) + return Save(state, path.Base(state.Url.Path)) } -func Outline(state *BrowserState, conf *Config) error { +func Outline(state *BrowserState) error { if state.Body == nil { return ErrMustBeOnAPage } @@ -604,7 +646,7 @@ func Outline(state *BrowserState, conf *Config) error { } } - formatted, _, err := parseGemtextDoc(b.Bytes(), conf.SoftWrap) + formatted, _, err := parseGemtextDoc(b.Bytes(), state.SoftWrap) if err != nil { return err } @@ -617,7 +659,7 @@ func Outline(state *BrowserState, conf *Config) error { return Print(state) } -func Links(state *BrowserState, conf *Config) error { +func Links(state *BrowserState) error { if state.Links == nil { return ErrMustBeOnAPage } @@ -626,7 +668,7 @@ func Links(state *BrowserState, conf *Config) error { for _, link := range state.Links { fmt.Fprintf(buf, "=> %s %s\n", link.Target.String(), link.Text) } - formatted, _, err := parseDoc("text/gemini", buf.Bytes(), conf) + formatted, _, err := parseDoc("text/gemini", buf.Bytes(), state.Config) if err != nil { return err } @@ -666,7 +708,7 @@ func HistoryCmd(state *BrowserState) error { return Print(state) } -func Save(state *BrowserState, filename string, conf *Config) error { +func Save(state *BrowserState, filename string) error { if state.Body == nil { return ErrMustBeOnAPage } @@ -674,7 +716,7 @@ func Save(state *BrowserState, filename string, conf *Config) error { return ErrSaveNeedsFilename } - p := filepath.Join(conf.DownloadFolder, filename) + p := filepath.Join(state.DownloadFolder, filename) _, err := os.Stat(p) pbase := p i := 1 @@ -691,12 +733,12 @@ func Save(state *BrowserState, filename string, conf *Config) error { return Print(state) } -func Mark(state *BrowserState, args []string, conf *Config) error { +func Mark(state *BrowserState, args []string) error { switch args[0] { case "add": - return MarkAdd(state, conf, args[1], args[2]) + return MarkAdd(state, args[1], args[2]) case "go": - return MarkGo(state, conf, args[1]) + return MarkGo(state, args[1]) case "list": return MarkList(state) case "delete": @@ -706,27 +748,27 @@ func Mark(state *BrowserState, args []string, conf *Config) error { return ErrInvalidMarkArgs } -func TourCmd(state *BrowserState, args []string, conf *Config) error { +func TourCmd(state *BrowserState, args []string) error { switch args[0] { case "add": if args[1] == "next" { - return TourAddNext(state, conf, args[2:]) + return TourAddNext(state, args[2:]) } - return TourAdd(state, conf, args[1:]) + return TourAdd(state, args[1:]) case "show": return TourShow(state) case "select": return TourSelect(state, args[1]) case "next": - return TourNext(state, conf) + return TourNext(state) case "previous": - return TourPrevious(state, conf) + return TourPrevious(state) case "clear": return TourClear(state) case "list": return TourList(state) case "go": - return TourGo(state, conf, args[1]) + return TourGo(state, args[1]) } return ErrInvalidTourArgs diff --git a/command.go b/command.go index db0c6a2..e231c7a 100644 --- a/command.go +++ b/command.go @@ -368,54 +368,54 @@ func parseIdentityArgs(line string) ([]string, error) { return nil, ErrInvalidArgs } -func RunCommand(conf *Config, cmd *Command, state *BrowserState) error { +func RunCommand(cmd *Command, state *BrowserState) error { switch cmd.Name { case "about": return About(state) case "root": - return Root(state, true, conf) + return Root(state, true) case "Root": - return Root(state, false, conf) + return Root(state, false) case "reload": - return Reload(state, conf) + return Reload(state) case "back": num := 1 if len(cmd.Args) == 1 { num, _ = strconv.Atoi(cmd.Args[0]) } - return Back(state, conf, num) + return Back(state, num) case "forward": num := 1 if len(cmd.Args) == 1 { num, _ = strconv.Atoi(cmd.Args[0]) } - return Forward(state, conf, num) + return Forward(state, num) case "next": - return Next(state, conf) + return Next(state) case "previous": - return Previous(state, conf) + return Previous(state) case "up": - return Up(state, conf) + return Up(state) case "go": - return Go(state, cmd.Args[0], conf) + return Go(state, cmd.Args[0]) case "help": return Help(state, cmd.Args[0]) case "outline": - return Outline(state, conf) + return Outline(state) case "pipe": return Pipe(state, cmd.Args[0]) case "print": return Print(state) case "links": - return Links(state, conf) + return Links(state) case "history": return HistoryCmd(state) case "save": - return Save(state, cmd.Args[0], conf) + return Save(state, cmd.Args[0]) case "mark": - return Mark(state, cmd.Args, conf) + return Mark(state, cmd.Args) case "tour": - return TourCmd(state, cmd.Args, conf) + return TourCmd(state, cmd.Args) case "identity": return IdentityCmd(state, cmd.Args) case "quit": diff --git a/files.go b/files.go index 4964750..b344e11 100644 --- a/files.go +++ b/files.go @@ -17,13 +17,14 @@ import ( ) type ConfigMain struct { - DefaultScheme string `toml:"default_scheme"` - SoftWrap int `toml:"soft_wrap"` - DownloadFolder string `toml:"download_folder"` - VimKeys bool `toml:"vim_keys"` - Quiet bool `toml:"quiet"` - Pager string `toml:"pager"` - Timeout duration `toml:"duration"` + DefaultScheme string `toml:"default_scheme"` + SoftWrap int `toml:"soft_wrap"` + DownloadFolder string `toml:"download_folder"` + VimKeys bool `toml:"vim_keys"` + Quiet bool `toml:"quiet"` + Pager string `toml:"pager"` + Timeout duration `toml:"duration"` + SavedHistoryDepth int `toml:"saved_history_depth"` } type Config struct { @@ -63,6 +64,7 @@ func getConfig() (*Config, error) { Timeout: duration{ time.Duration(10 * time.Second), }, + SavedHistoryDepth: 30, }, Handlers: map[string]string{}, } diff --git a/go.mod b/go.mod index 5da2fb6..5a3ef25 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,22 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v0.25.0 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect - golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 8b7c35b..8cebcc5 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,59 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +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-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= tildegit.org/tjp/sliderule v1.6.2-0.20240115025310-751f423f11bf h1:p0MqM4m/LcgLjRH24OOV+oNOu/8+alABAdI6kLantvE= diff --git a/handlers.go b/handlers.go index 64ea5c0..599d53b 100644 --- a/handlers.go +++ b/handlers.go @@ -137,10 +137,10 @@ const ( promptStyle = "\x1b[38;5;39m" quoteStyle = "\x1b[38;5;208m\x1b[3m" rawStyle = "\x1b[38;5;249m" - h1Style = "\x1b[38;5;154m\x1b[1m\x1b[4m" - h2Style = "\x1b[38;5;50m\x1b[4m" - h3Style = "\x1b[38;5;6m\x1b[4m" - listStyle = "\x1b[38;5;3m" + h1Style = "\x1b[38;5;154m\x1b[1m\x1b[4m" + h2Style = "\x1b[38;5;50m\x1b[4m" + h3Style = "\x1b[38;5;6m\x1b[4m" + listStyle = "\x1b[38;5;3m" ) func parseGemtextDoc(body []byte, softWrap int) (string, []Link, error) { @@ -189,7 +189,7 @@ func parseGemtextDoc(body []byte, softWrap int) (string, []Link, error) { i += 1 case gemtext.LineTypeQuote: q := item.(gemtext.QuoteLine) - for _, line := range fold(q.Body(), softWrap - 1) { + for _, line := range fold(q.Body(), softWrap-1) { line = strings.TrimSpace(line) if _, err := b.WriteString(textpad + "> " + quoteStyle + line + ansiClear + "\n"); err != nil { return "", nil, err @@ -219,15 +219,16 @@ func parseGemtextDoc(body []byte, softWrap int) (string, []Link, error) { case 3: color = h3Style } - + for _, line := range fold(item.String(), softWrap) { + line = strings.TrimRight(line, "\r\n") if _, err := b.WriteString(textpad + color + line + ansiClear + "\n"); err != nil { return "", nil, err } } case gemtext.LineTypeListItem: li := item.(gemtext.ListItemLine) - for i, line := range fold(li.Body(), softWrap - 2) { + for i, line := range fold(li.Body(), softWrap-2) { lpad := " " if i == 0 { lpad = "* " diff --git a/identity.go b/identity.go index e864171..fea5722 100644 --- a/identity.go +++ b/identity.go @@ -156,10 +156,10 @@ func IdentityUseDomain(state *BrowserState, name string, domain string) error { ident := state.Identities.ByName[name] u, _, err := parseURL(domain, state, "gemini") - if errors.Is(err, ErrInvalidLink) { + if errors.Is(err, invalidLinkErr("")) { u, err = url.Parse(domain) if err != nil { - return ErrInvalidLink + return ErrInvalidLink(domain) } if u.Hostname() == "" { u.Host = domain @@ -185,10 +185,10 @@ func IdentityUseFolder(state *BrowserState, name string, domain string) error { ident := state.Identities.ByName[name] u, _, err := parseURL(domain, state, "gemini") - if errors.Is(err, ErrInvalidLink) { + if errors.Is(err, invalidLinkErr("")) { u, err = url.Parse(domain) if err != nil { - return ErrInvalidLink + return ErrInvalidLink(domain) } if u.Hostname() == "" { u.Host = domain @@ -215,10 +215,10 @@ func IdentityUsePage(state *BrowserState, name string, domain string) error { ident := state.Identities.ByName[name] u, _, err := parseURL(domain, state, "gemini") - if errors.Is(err, ErrInvalidLink) { + if errors.Is(err, invalidLinkErr("")) { u, err = url.Parse(domain) if err != nil { - return ErrInvalidLink + return ErrInvalidLink(domain) } if u.Hostname() == "" { u.Host = domain diff --git a/main.go b/main.go index eb85922..b2f5d98 100644 --- a/main.go +++ b/main.go @@ -11,40 +11,18 @@ import ( "github.com/chzyer/readline" ) -var cmdMode = flag.String("c", "", "") -var helpMode = flag.Bool("h", false, "") -var quietMode = flag.Bool("q", false, "") +var ( + cmdMode = flag.String("c", "", "") + helpMode = flag.Bool("h", false, "") + quietMode = flag.Bool("q", false, "") +) func main() { - conf, err := getConfig() + state, err := buildInitialState() if err != nil { log.Fatal(err) } - if err := getTofuStore(); err != nil { - log.Fatal(err) - } - - state := NewBrowserState(conf) - - marks, err := getMarks() - if err != nil { - log.Fatal(err) - } - state.Marks = marks - - tours, err := getTours() - if err != nil { - log.Fatal(err) - } - state.NamedTours = tours - - idents, err := getIdentities() - if err != nil { - log.Fatal(err) - } - state.Identities = idents - flag.Parse() if *helpMode { @@ -55,9 +33,8 @@ func main() { } if *cmdMode != "" { - conf.Quiet = true state.Quiet = true - if err := handleCmdLine(state, conf, *cmdMode); err != nil { + if err := handleCmdLine(state, *cmdMode); err != nil { writeError(err.Error()) } return @@ -67,25 +44,72 @@ func main() { state.Quiet = true } - rl, err := readline.New(Prompt) + if urls := flag.Args(); len(urls) > 0 { + if err := Go(state, urls[0]); err != nil { + writeError(err.Error()) + } + } + + // runInteractivePrompt(state) + runTUI(state) +} + +func buildReadline(prompt string, conf *Config) (*readline.Instance, error) { + rl, err := readline.New(prompt) if err != nil { - log.Fatal(err) + return nil, err } if conf.VimKeys { rl.SetVimMode(true) } - state.Readline = rl - if urls := flag.Args(); len(urls) > 0 { - if err := Go(state, urls[0], conf); err != nil { - writeError(err.Error()) - } + return rl, nil +} + +func buildInitialState() (*BrowserState, error) { + conf, err := getConfig() + if err != nil { + return nil, err } + if err := getTofuStore(); err != nil { + return nil, err + } + + state := NewBrowserState(conf) + + marks, err := getMarks() + if err != nil { + return nil, err + } + state.Marks = marks + + tours, err := getTours() + if err != nil { + return nil, err + } + state.NamedTours = tours + + idents, err := getIdentities() + if err != nil { + return nil, err + } + state.Identities = idents + + rl, err := buildReadline(Prompt, conf) + if err != nil { + log.Fatal(err) + } + state.Readline = rl + + return state, nil +} + +func runInteractivePrompt(state *BrowserState) { for { - rl.SetPrompt(Prompt) - line, err := rl.Readline() + state.Readline.SetPrompt(Prompt) + line, err := state.Readline.Readline() if err == io.EOF { break } @@ -93,17 +117,17 @@ func main() { log.Fatal(err) } - if err := handleCmdLine(state, conf, line); err != nil { + if err := handleCmdLine(state, line); err != nil { writeError(err.Error()) } } } -func handleCmdLine(state *BrowserState, conf *Config, line string) error { +func handleCmdLine(state *BrowserState, line string) error { for _, cmd := range strings.Split(line, ";") { if c, err := ParseCommand(strings.TrimSpace(cmd)); err != nil { return err - } else if err := RunCommand(conf, c, state); err != nil { + } else if err := RunCommand(c, state); err != nil { return err } } diff --git a/mark.go b/mark.go index c162144..e1e611e 100644 --- a/mark.go +++ b/mark.go @@ -12,8 +12,8 @@ var ( ErrNotAMark = errors.New("that's not a known mark name") ) -func MarkAdd(state *BrowserState, conf *Config, name, target string) error { - u, _, err := parseURL(target, state, conf.DefaultScheme) +func MarkAdd(state *BrowserState, name, target string) error { + u, _, err := parseURL(target, state, state.DefaultScheme) if err != nil { return ErrInvalidURL } @@ -27,13 +27,13 @@ func MarkAdd(state *BrowserState, conf *Config, name, target string) error { return Print(state) } -func MarkGo(state *BrowserState, conf *Config, name string) error { +func MarkGo(state *BrowserState, name string) error { _, target, err := findMark(state, name) if err != nil { return err } - return Go(state, target, conf) + return Go(state, target) } func MarkList(state *BrowserState) error { diff --git a/state.go b/state.go index c46862f..57767a8 100644 --- a/state.go +++ b/state.go @@ -2,13 +2,13 @@ package main import ( "net/url" - "time" "github.com/chzyer/readline" ) type BrowserState struct { *History + *Config Modal []byte @@ -20,10 +20,6 @@ type BrowserState struct { DefaultTour Tour CurrentTour *Tour - Quiet bool - Pager string - Timeout time.Duration - Readline *readline.Instance } @@ -59,10 +55,7 @@ func NewBrowserState(conf *Config) *BrowserState { Depth: 0, NavIndex: -1, }, - - Quiet: conf.Quiet, - Pager: conf.Pager, - Timeout: conf.Timeout.Duration, + Config: conf, } state.CurrentTour = &state.DefaultTour return state diff --git a/tour.go b/tour.go index 1f60ee9..7e4e895 100644 --- a/tour.go +++ b/tour.go @@ -49,10 +49,10 @@ func parseURLs(state *BrowserState, defaultScheme, str string) ([]*url.URL, erro return []*url.URL{u}, nil } -func TourAdd(state *BrowserState, conf *Config, targets []string) error { +func TourAdd(state *BrowserState, targets []string) error { newurls := []*url.URL{} for _, target := range targets { - urls, err := parseURLs(state, conf.DefaultScheme, target) + urls, err := parseURLs(state, state.DefaultScheme, target) if err != nil { return err } @@ -69,10 +69,10 @@ func TourAdd(state *BrowserState, conf *Config, targets []string) error { return Print(state) } -func TourAddNext(state *BrowserState, conf *Config, targets []string) error { +func TourAddNext(state *BrowserState, targets []string) error { newurls := []*url.URL{} for _, target := range targets { - urls, err := parseURLs(state, conf.DefaultScheme, target) + urls, err := parseURLs(state, state.DefaultScheme, target) if err != nil { return err } @@ -113,7 +113,7 @@ func TourShow(state *BrowserState) error { return Print(state) } -func TourNext(state *BrowserState, conf *Config) error { +func TourNext(state *BrowserState) error { tour := state.CurrentTour if tour.Index >= len(tour.Links) || len(tour.Links) == 0 { return ErrEndOfTour @@ -121,10 +121,10 @@ func TourNext(state *BrowserState, conf *Config) error { page := tour.Links[tour.Index] tour.Index += 1 - return Navigate(state, page, -1, conf) + return Navigate(state, page, -1) } -func TourPrevious(state *BrowserState, conf *Config) error { +func TourPrevious(state *BrowserState) error { tour := state.CurrentTour if tour.Index <= 0 { return ErrStartOfTour @@ -135,7 +135,7 @@ func TourPrevious(state *BrowserState, conf *Config) error { } page := tour.Links[tour.Index-1] - return Navigate(state, page, -1, conf) + return Navigate(state, page, -1) } func TourClear(state *BrowserState) error { @@ -173,7 +173,7 @@ func TourList(state *BrowserState) error { return Print(state) } -func TourGo(state *BrowserState, conf *Config, pos string) error { +func TourGo(state *BrowserState, pos string) error { tour := state.CurrentTour i, _ := strconv.Atoi(pos) @@ -182,7 +182,7 @@ func TourGo(state *BrowserState, conf *Config, pos string) error { } tour.Index = i + 1 - return Navigate(state, tour.Links[i], -1, conf) + return Navigate(state, tour.Links[i], -1) } func TourSelect(state *BrowserState, name string) error { diff --git a/tui.go b/tui.go new file mode 100644 index 0000000..12bb665 --- /dev/null +++ b/tui.go @@ -0,0 +1,43 @@ +package main + +import ( + "os" + + tea "github.com/charmbracelet/bubbletea" +) + +type TUIModel struct { + State *BrowserState +} + +func NewTUIModel(state *BrowserState) TUIModel { + return TUIModel{State: state} +} + +func (model TUIModel) Init() tea.Cmd { + return nil +} + +func (model TUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "ctrl+d", "q": + return model, tea.Quit + } + } + + return model, nil +} + +func (model TUIModel) View() string { + return "pardon our dust" +} + +func runTUI(state *BrowserState) { + p := tea.NewProgram(NewTUIModel(state)) + if _, err := p.Run(); err != nil { + writeError(err.Error()) + os.Exit(1) + } +}