skipper/main.go

323 lines
6.5 KiB
Go

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