summaryrefslogtreecommitdiff
path: root/bin/gemini
blob: 1bc3d57f68c40606ccab481e442162c696da012c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env yaegi
package main

import (
	"bufio"
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"net/url"
	"os"
	"strconv"
	"strings"
)

func main() {
	stdinReader := bufio.NewReader(os.Stdin)
	var u string // URL
	links := make([]string, 0, 100)
	history := make([]string, 0, 100)
	for {
		fmt.Print("> ")
		cmd, _ := stdinReader.ReadString('\n')
		cmd = strings.TrimSpace(cmd)
		// Command dispatch
		switch strings.ToLower(cmd) {
		case "": // Nothing
			continue
		case "q": // Quit
			fmt.Println("Bye!")
			os.Exit(0)
		case "b": // Back
			if len(history) < 2 {
				fmt.Println("No history yet!")
				continue
			}
			u = history[len(history)-2]
			history = history[0 : len(history)-2]
		default:
			index, err := strconv.Atoi(cmd)
			if err != nil {
				// Treat this as a URL
				u = cmd
				if !strings.HasPrefix(u, "gemini://") {
					u = "gemini://" + u
				}
			} else {
				// Treat this as a menu lookup
				u = links[index-1]
			}
		}
		// Parse URL
		parsed, err := url.Parse(u)
		if err != nil {
			fmt.Println("Error parsing URL!")
			continue
		}
		// Connect to server
		conn, err := tls.Dial("tcp", parsed.Host+":1965", &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			fmt.Println("Failed to connect: " + err.Error())
			continue
		}
		defer conn.Close()
		// Send request
		conn.Write([]byte(u + "\r\n"))
		// Receive and parse response header
		reader := bufio.NewReader(conn)
		responseHeader, err := reader.ReadString('\n')
		parts := strings.Fields(responseHeader)
		status, err := strconv.Atoi(parts[0][0:1])
		meta := parts[1]
		// Switch on status code
		switch status {
		case 1, 3, 6:
			// No input, redirects or client certs
			fmt.Println("Unsupported feature!")
		case 2:
			// Successful transaction
			// text/* content only
			if !strings.HasPrefix(meta, "text/") {
				fmt.Println("Unsupported type " + meta)
				continue
			}
			// Read everything
			bodyBytes, err := ioutil.ReadAll(reader)
			if err != nil {
				fmt.Println("Error reading body")
				continue
			}
			body := string(bodyBytes)
			if meta == "text/gemini" {
				// Handle Gemini map
				links = make([]string, 0, 100)
				preformatted := false
				for _, line := range strings.Split(body, "\n") {
					if strings.HasPrefix(line, "```") {
						preformatted = !preformatted
					} else if preformatted {
						fmt.Println(line)
					} else if strings.HasPrefix(line, "=>") {
						line = line[2:]
						bits := strings.Fields(line)
						parsedLink, err := url.Parse(bits[0])
						if err != nil {
							continue
						}
						link := parsed.ResolveReference(parsedLink).String()
						var label string
						if len(bits) == 1 {
							label = link
						} else {
							label = strings.Join(bits[1:], " ")
						}
						links = append(links, link)
						fmt.Printf("[%d] %s\n", len(links), label)
					} else {
						// This should really be wrapped, but there's
						// no easy support for this in Go's standard
						// library
						fmt.Println(line)
					}
				}
			} else {
				// Just print any other kind of text
				fmt.Print(body)
			}
			history = append(history, u)
		case 4, 5:
			fmt.Println("ERROR: " + meta)
		}
	}
}