323 lines
6.5 KiB
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()
|
|
}
|