start of a bubbletea TUI

This commit is contained in:
tjp 2024-01-17 08:55:42 -07:00
parent dfebc9013b
commit dd2a06c1e1
12 changed files with 310 additions and 154 deletions

View File

@ -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

View File

@ -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":

View File

@ -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{},
}

17
go.mod
View File

@ -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
)

36
go.sum
View File

@ -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=

View File

@ -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 = "* "

View File

@ -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

108
main.go
View File

@ -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
}
}

View File

@ -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 {

View File

@ -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

20
tour.go
View File

@ -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 {

43
tui.go Normal file
View File

@ -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)
}
}