package main // import "net/http" // import _ "net/http/pprof" import "git.tebibyte.media/sashakoshka/skipper/dom" import "git.tebibyte.media/sashakoshka/skipper/input" import "os" import "fmt" import "sync" import "bytes" import "image" import "net/url" import "strings" import "strconv" import _ "embed" import _ "image/png" import "git.sr.ht/~yotam/go-gemini" import "git.tebibyte.media/sashakoshka/stone" import "git.tebibyte.media/sashakoshka/stone/config" import _ "git.tebibyte.media/sashakoshka/stone/backends/x" //go:embed icon/icon64.png var iconBytes []byte var application = &stone.Application { } var client = gemini.Client { InsecureSkipVerify: true } var browserConfig = config.Config { LegalParameters: map[string] config.Type { "homePage": config.TypeString, "searchEngine": config.TypeString, }, Parameters: map[string] any { "homePage": "about:home", "searchEngine": "gemini://geminispace.info:1965/search/", }, } var page struct { history History document *dom.Document currentUrl *url.URL loading bool scroll int height int redirectCounter int viewHeight int input input.Input readingInput bool urlInput input.Input readingUrlInput bool redrawLock sync.Mutex } var inputState struct { x int y int } func loadImage (name string) (output image.Image) { file, err := os.Open(name) defer file.Close() if err != nil { panic(err) } output, _, err = image.Decode(file) if err != nil { panic(err) } return } func main () { // go func() { // println(http.ListenAndServe("localhost:7070", nil)) // } () application.SetTitle("Skipper") application.SetSize(80, 24) icon, _, err := image.Decode(bytes.NewReader(iconBytes)) if err != nil { panic(err) } application.SetIcon([]image.Image { icon }) application.OnStart(onStart) application.OnResize(onResize) application.OnPress(onPress) application.OnMouseMove(onMouseMove) application.OnScroll(onScroll) page.input.OnDone = onInputPageDone page.input.OnCancel = onInputPageCancel page.urlInput.OnDone = onUrlBarDone page.urlInput.OnCancel = onUrlBarCancel err = application.Run() if err != nil { panic(err) } } func onStart () { browserConfig.Load("skipper") location, _ := url.Parse(browserConfig.Parameters["homePage"].(string)) go fetch(location) } func onResize () { redraw() constrainScroll() } func onPress (button stone.Button, modifiers stone.Modifiers) { switch button { case stone.MouseButtonLeft: if inputState.y >= page.viewHeight { onUrlBarSelect(page.currentUrl.String()) } else { actualY := inputState.y + page.scroll element := page.document.ElementAtY(actualY) hyperlink, isHyperlink := element.(*dom.ElementHyperlink) if !isHyperlink { break } location := normalizeDocumentUrl(hyperlink.Location()) go fetch(location,) } default: } if input.Selected() != nil { input.Selected().HandleButton ( button, modifiers.Control, modifiers.Shift, modifiers.Alt) redrawInputs() application.Draw() return } action, bound := bindings[bindingKey { control: modifiers.Control, shift: modifiers.Shift, alt: modifiers.Alt, button: button, }] if bound && action != nil { action() return } } func onMouseMove (x, y int) { inputState.x = x inputState.y = y } func onScroll (x, y int) { page.scroll += y * 4 constrainScroll() redraw() application.Draw() } func onInputPageDone () { location := *page.currentUrl location.RawQuery = page.input.Text() page.readingInput = false go fetch(&location) } func onInputPageCancel () { page.readingInput = false fetchBackward() } func onUrlBarSelect (startingText string) { page.urlInput.SetText(startingText) page.urlInput.Select() page.readingUrlInput = true redrawInputs() application.Draw() } func onUrlBarDone () { page.readingUrlInput = false inputText := page.urlInput.Text() if strings.HasPrefix(inputText, "/") { inputText = inputText[1:] // TODO: find in page } else if strings.HasPrefix(inputText, ".") { inputText = inputText[1:] linkId, err := strconv.Atoi(inputText) if err != nil { return } followLink(linkId) } else if strings.HasPrefix(inputText, "gemini://") { location, err := url.Parse(inputText) if err != nil { location, _ := url.Parse ( browserConfig.Parameters["searchEngine"]. (string)) location.RawQuery = inputText go fetch(location) return } go fetch(location) } else { location, _ := url.Parse ( browserConfig.Parameters["searchEngine"].(string)) location.RawQuery = inputText go fetch(location) } } func onUrlBarCancel () { page.readingUrlInput = false redrawStatus() application.Draw() } func constrainScroll () (constrained bool) { if page.scroll < 0 { page.scroll = 0 constrained = true } if page.scroll > page.height - 4 { page.scroll = page.height - 4 constrained = true } return } func followLink (id int) { location := page.document.UrlAtId(id) if location == nil { return } location = normalizeDocumentUrl(location) go fetch(location) } func normalizeDocumentUrl (strange *url.URL) (normal *url.URL) { normal = strange.JoinPath("") if !normal.IsAbs() { normal = page.currentUrl.JoinPath ( normal.String()) normal.RawQuery = "" } if !strings.Contains(normal.Host, ":") { normal.Host += ":1965" } return } func redrawInputs () { if page.readingUrlInput { page.urlInput.Draw(application) } else if page.readingInput { page.input.Draw(application) } } func redrawStatus () { width, height := application.Size() for x := 0; x < width; x ++ { application.SetRune(x, height - 1, 0) application.SetStyle(x, height - 1, stone.StyleNormal) } application.SetDot(0, height - 1) fmt.Fprint(application, page.currentUrl) if page.loading { application.SetDot(width - 7, height - 1) fmt.Fprint(application, "loading") } } func redraw () { page.redrawLock.Lock() defer page.redrawLock.Unlock() width, height := application.Size() documentWidth := width if documentWidth > 90 { documentWidth = 90 } documentMargin := (width - documentWidth) / 2 application.Clear() if page.document != nil { page.height = page.document.Render(application, 0 - page.scroll) } for y := height - 2; y < height; y ++ { for x := 0; x < width; x ++ { application.SetRune(x, y, 0) application.SetColor(x, y, stone.ColorDim) }} redrawStatus() page.viewHeight = height - 2 page.input.X = documentMargin page.input.Y = page.height + 1 page.input.Width = documentWidth page.urlInput.X = 0 page.urlInput.Y = height - 1 page.urlInput.Width = width redrawInputs() }