gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

commit 8ce55a6cdc7a38d876b01bf5f1aef0068698e99b
parent 92fd54e0416c6ffd0d54053f04222f00d828f57b
Author: Andrew Laack <andrew@laack.co>
Date:   Wed,  6 May 2026 22:02:53 -0500

Starting to understand the rendering stuff

Diffstat:
Mmain.go | 46+++++++++++++++++++++++++++++++++++++++++++++-
Aparse.go | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 147 insertions(+), 1 deletion(-)

diff --git a/main.go b/main.go @@ -18,16 +18,60 @@ func main(){ panic(err) } + bodyBytes, err := io.ReadAll(resp.Body) + resp.Body.Close() + text := string(bodyBytes) + // links := ParseLinks(text, os.Args[1]) app := tview.NewApplication() textView := tview.NewTextView() textView.SetBackgroundColor(tcell.ColorDefault) - textView.SetText(text) + + selectionMode := false + + textView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Rune() { + case ' ': + selectionMode = !selectionMode + case '1', '2','3','4','5','6','7','8','9','0': + if selectionMode { + selectionMode = false + go func() { + client := &gemini.Client{ConnectTimeout: 5 * time.Second} + resp, err := client.Fetch("gemini://blog.laack.co") + + if err != nil { + panic(err) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + resp.Body.Close() + + textNew := string(bodyBytes) + + app.QueueUpdateDraw(func() { + textView.SetText(textNew) + }) + }() + + return nil + } + } + + return event + }) + + + if err := app.SetRoot(textView, true).SetFocus(textView).Run(); err != nil { panic(err) diff --git a/parse.go b/parse.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "net/url" + "strings" +) + +func stripLeadingWhiteSpace(text string) string { + + for len(text) > 0 { + if text[0] == ' ' || text[0] == '\t' { + if len(text) > 1 { + text = text[1:] + } else { + text = "" + return text + } + } else { + return text + } + } + + return text +} + +func ParseLinks(body string, currentUrl string) []string { + + base, err := url.Parse(currentUrl) + + if err != nil { + panic(err) + } + + lines := strings.Split(body, "\n") + + links := []string{} + + escaped := false + escape := "```" + + for _, item := range lines { + + // must start with escape characters, the rest doesn't matter to us + // > Any line whose first three characters are "```" (...) are preformatted toggle lines + if len(item) >= 3 && strings.Compare(escape, item[:3]) == 0 { + escaped = !escaped + } + + if len(item) > 3 && !escaped { + if item[0] == '=' && item[1] == '>' { + + // sometimes links end with a \r, but that isn't valid so we won't allow it + links = append(links, stripLeadingWhiteSpace(item[2:])) + } + } + + } + + geminiLinks := []string{} + + for _, item := range links { + + // this is for finding the text associated with the link + + indexOfSpace := strings.Index(item, " ") + indexOfTab := strings.Index(item, "\t") + + // default if there aren't any + indexOfSpaceOrTab := len(item) + + if indexOfSpace != -1 { + indexOfSpaceOrTab = indexOfSpace + } + + if indexOfTab != -1 { + if indexOfTab < indexOfSpace || indexOfSpace == -1 { + indexOfSpaceOrTab = indexOfTab + } + } + + if len(item) >= 10 && strings.Compare(item[:9], "gemini://") == 0 { + geminiLinks = append(geminiLinks, item[0:indexOfSpaceOrTab]) + } + + // there are urls that aren't relative that don't have // like like mailto: and monero: + if strings.Contains(item, ":") == false { + // relative link + + u, err := url.Parse(item[0:indexOfSpaceOrTab]) + + if err != nil { + fmt.Printf("Unable to parse link: %s, %s\n", item, err) + continue + } + + geminiLinks = append(geminiLinks, base.ResolveReference(u).String()) + } + } + + return geminiLinks +}