summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--actions.go148
-rw-r--r--command.go30
-rw-r--r--files.go16
-rw-r--r--go.mod17
-rw-r--r--go.sum36
-rw-r--r--handlers.go15
-rw-r--r--identity.go12
-rw-r--r--main.go108
-rw-r--r--mark.go8
-rw-r--r--state.go11
-rw-r--r--tour.go20
-rw-r--r--tui.go43
12 files changed, 310 insertions, 154 deletions
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,39 +11,17 @@ 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()
- if err != nil {
- log.Fatal(err)
- }
-
- if err := getTofuStore(); err != nil {
- log.Fatal(err)
- }
-
- state := NewBrowserState(conf)
-
- marks, err := getMarks()
+ state, err := buildInitialState()
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()
@@ -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)
+ }
+}