Apparently added a ridiculous mess since last commit
This commit is contained in:
parent
a28fc00550
commit
80ab652448
|
@ -0,0 +1,170 @@
|
||||||
|
package cmdparse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"strings"
|
||||||
|
"io"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + T Y P E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
kind tok
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
|
||||||
|
type scanner struct {
|
||||||
|
r *bufio.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
type tok int
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + V A R I A B L E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
var eof rune = rune(0)
|
||||||
|
const (
|
||||||
|
Word tok = iota
|
||||||
|
Action
|
||||||
|
Value
|
||||||
|
End
|
||||||
|
Whitespace
|
||||||
|
|
||||||
|
number
|
||||||
|
letter
|
||||||
|
ws
|
||||||
|
illegal
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + R E C E I V E R S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
func (s *scanner) read() rune {
|
||||||
|
ch, _, err := s.r.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return eof
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) scanText() Token {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteRune(s.read())
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ch := s.read(); ch == eof {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else if !isLetter(ch) && !isDigit(ch) {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
_, _ = buf.WriteRune(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capInput := strings.ToUpper(buf.String())
|
||||||
|
switch capInput {
|
||||||
|
case "DELETE", "ADD", "WRITE", "SET", "RECALL", "R",
|
||||||
|
"W", "A", "D", "S", "Q", "QUIT", "B", "BOOKMARKS", "H", "HOME":
|
||||||
|
return Token{Action, capInput}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Token{Word, buf.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) scanWhitespace() Token {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteRune(s.read())
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ch := s.read(); ch == eof {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else if !isWhitespace(ch) {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
_,_ = buf.WriteRune(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Token{Whitespace, buf.String()}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) scanNumber() Token {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteRune(s.read())
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ch := s.read(); ch == eof {
|
||||||
|
break
|
||||||
|
} else if !isDigit(ch) {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
_,_ = buf.WriteRune(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Token{Value, buf.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) unread() { _ = s.r.UnreadRune() }
|
||||||
|
|
||||||
|
func (s *scanner) scan() Token {
|
||||||
|
char := s.read()
|
||||||
|
|
||||||
|
if isWhitespace(char) {
|
||||||
|
s.unread()
|
||||||
|
return s.scanWhitespace()
|
||||||
|
} else if isDigit(char) {
|
||||||
|
s.unread()
|
||||||
|
return s.scanNumber()
|
||||||
|
} else if isLetter(char) {
|
||||||
|
s.unread()
|
||||||
|
return s.scanText()
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == eof {
|
||||||
|
return Token{End, ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Token{illegal, string(char)}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + F U N C T I O N S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
func NewScanner(r io.Reader) *scanner {
|
||||||
|
return &scanner{r: bufio.NewReader(r)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWhitespace(ch rune) bool {
|
||||||
|
return ch == ' ' || ch == '\t' || ch == '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLetter(ch rune) bool {
|
||||||
|
return ch >= '!' && ch <= '~'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDigit(ch rune) bool {
|
||||||
|
return ch >= '0' && ch <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEOF(ch rune) bool {
|
||||||
|
return ch == rune(0)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
package cmdparse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + T Y P E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
s *scanner
|
||||||
|
buffer struct {
|
||||||
|
token Token
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Action string
|
||||||
|
Target string
|
||||||
|
Value []string
|
||||||
|
Type Comtype
|
||||||
|
}
|
||||||
|
|
||||||
|
type Comtype int
|
||||||
|
const (
|
||||||
|
GOURL Comtype = iota
|
||||||
|
GOLINK
|
||||||
|
SIMPLE
|
||||||
|
DOLINK
|
||||||
|
DOLINKAS
|
||||||
|
DOAS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + R E C E I V E R S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
func (p *Parser) scan() (current Token) {
|
||||||
|
if p.buffer.size != 0 {
|
||||||
|
p.buffer.size = 0
|
||||||
|
return p.buffer.token
|
||||||
|
}
|
||||||
|
|
||||||
|
current = p.s.scan()
|
||||||
|
for {
|
||||||
|
if current.kind != Whitespace {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
current = p.s.scan()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.buffer.token = current
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) unscan() { p.buffer.size = 1 }
|
||||||
|
|
||||||
|
func (p *Parser) parseNonAction() (*Command, error) {
|
||||||
|
p.unscan()
|
||||||
|
t := p.scan()
|
||||||
|
cm := &Command{}
|
||||||
|
|
||||||
|
if t.kind == Value {
|
||||||
|
cm.Target = t.val
|
||||||
|
cm.Type = GOLINK
|
||||||
|
} else if t.kind == Word {
|
||||||
|
cm.Target = t.val
|
||||||
|
cm.Type = GOURL
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("Found %q, expected action, url, or link number", t.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u := p.scan(); u.kind != End {
|
||||||
|
return nil, fmt.Errorf("Found %q, expected EOF", u.val)
|
||||||
|
}
|
||||||
|
return cm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseAction() (*Command, error) {
|
||||||
|
p.unscan()
|
||||||
|
t := p.scan()
|
||||||
|
cm := &Command{}
|
||||||
|
cm.Action = t.val
|
||||||
|
t = p.scan()
|
||||||
|
switch t.kind {
|
||||||
|
case End:
|
||||||
|
cm.Type = SIMPLE
|
||||||
|
return cm, nil
|
||||||
|
case Value:
|
||||||
|
cm.Target = t.val
|
||||||
|
cm.Type = DOLINK
|
||||||
|
case Word:
|
||||||
|
cm.Value = append(cm.Value, t.val)
|
||||||
|
cm.Type = DOAS
|
||||||
|
case Action, Whitespace:
|
||||||
|
return nil, fmt.Errorf("Found %q (%d), expected value", t.val, t.kind)
|
||||||
|
}
|
||||||
|
t = p.scan()
|
||||||
|
if t.kind == End {
|
||||||
|
return cm, nil
|
||||||
|
} else {
|
||||||
|
if cm.Type == DOLINK {
|
||||||
|
cm.Type = DOLINKAS
|
||||||
|
} else {
|
||||||
|
cm.Type = DOAS
|
||||||
|
}
|
||||||
|
cm.Value = append(cm.Value, t.val)
|
||||||
|
|
||||||
|
for {
|
||||||
|
token := p.scan()
|
||||||
|
if token.kind == End {
|
||||||
|
break
|
||||||
|
} else if token.kind == Whitespace {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cm.Value = append(cm.Value, token.val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Parse() (*Command, error) {
|
||||||
|
if t := p.scan(); t.kind != Action {
|
||||||
|
return p.parseNonAction()
|
||||||
|
} else {
|
||||||
|
return p.parseAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + F U N C T I O N S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
func NewParser(r io.Reader) *Parser {
|
||||||
|
return &Parser{s: NewScanner(r)}
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + T Y P E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
kind TokenType
|
||||||
|
val string
|
||||||
|
}
|
||||||
|
|
||||||
|
type scanner struct {
|
||||||
|
r *bufio.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenType int
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + V A R I A B L E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
var eof rune = rune(0)
|
||||||
|
var l_brace rune = '['
|
||||||
|
var r_brace rune = ']'
|
||||||
|
var newline rune = '\n'
|
||||||
|
var equal rune = '='
|
||||||
|
|
||||||
|
|
||||||
|
const (
|
||||||
|
TOK_SECTION TokenType = iota
|
||||||
|
TOK_KEY
|
||||||
|
TOK_VALUE
|
||||||
|
|
||||||
|
TOK_EOF
|
||||||
|
TOK_NEWLINE
|
||||||
|
TOK_ERROR
|
||||||
|
TOK_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + R E C E I V E R S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
func (s *scanner) read() rune {
|
||||||
|
ch, _, err := s.r.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return eof
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) skipWhitespace() {
|
||||||
|
for {
|
||||||
|
if ch := s.read(); ch == eof {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else if !isWhitespace(ch) {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) skipToEndOfLine() {
|
||||||
|
for {
|
||||||
|
if ch := s.read(); ch == eof {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else if ch == newline {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) scanSection() Token {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteRune(s.read())
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ch := s.read(); ch == eof {
|
||||||
|
s.unread()
|
||||||
|
return Token{TOK_ERROR, "Reached end of feed without closing section"}
|
||||||
|
} else if ch == r_brace {
|
||||||
|
break
|
||||||
|
} else if ch == newline {
|
||||||
|
s.unread()
|
||||||
|
return Token{TOK_ERROR, "No closing brace for section before newline"}
|
||||||
|
} else if ch == equal {
|
||||||
|
return Token{TOK_ERROR, "Illegal character"}
|
||||||
|
} else if ch == l_brace {
|
||||||
|
s.skipToEndOfLine()
|
||||||
|
return Token{TOK_ERROR, "Second left brace encountered before closing right brace in section"}
|
||||||
|
} else {
|
||||||
|
_,_ = buf.WriteRune(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Token{TOK_SECTION, buf.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) scanKey() Token {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ch := s.read(); ch == eof {
|
||||||
|
s.unread()
|
||||||
|
return Token{TOK_ERROR, "Reached end of feed without assigning value to key"}
|
||||||
|
} else if ch == equal {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else if ch == newline {
|
||||||
|
s.unread()
|
||||||
|
return Token{TOK_ERROR, "No value assigned to key"}
|
||||||
|
} else if ch == r_brace || ch == l_brace {
|
||||||
|
s.skipToEndOfLine()
|
||||||
|
return Token{TOK_ERROR, "Illegal brace character in key"}
|
||||||
|
} else {
|
||||||
|
_,_ = buf.WriteRune(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Token{TOK_KEY, buf.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) scanValue() Token {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ch := s.read(); ch == eof {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else if ch == equal {
|
||||||
|
_,_ = buf.WriteRune(ch)
|
||||||
|
} else if ch == newline {
|
||||||
|
s.unread()
|
||||||
|
break
|
||||||
|
} else if ch == r_brace || ch == l_brace {
|
||||||
|
s.skipToEndOfLine()
|
||||||
|
return Token{TOK_ERROR, "Illegal brace character in key"}
|
||||||
|
} else {
|
||||||
|
_,_ = buf.WriteRune(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Token{TOK_VALUE, buf.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) unread() { _ = s.r.UnreadRune() }
|
||||||
|
|
||||||
|
func (s *scanner) scan() Token {
|
||||||
|
char := s.read()
|
||||||
|
|
||||||
|
if isWhitespace(char) {
|
||||||
|
s.skipWhitespace()
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == l_brace {
|
||||||
|
return s.scanSection()
|
||||||
|
} else if isText(char) {
|
||||||
|
s.unread()
|
||||||
|
return s.scanKey()
|
||||||
|
} else if char == equal {
|
||||||
|
return s.scanValue()
|
||||||
|
} else if char == newline {
|
||||||
|
return Token{TOK_NEWLINE, "New line"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == eof {
|
||||||
|
return Token{TOK_EOF, "Reached end of feed"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Token{TOK_ERROR, fmt.Sprintf("Error on character %q", char)}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + F U N C T I O N S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
func NewScanner(r io.Reader) *scanner {
|
||||||
|
return &scanner{r: bufio.NewReader(r)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWhitespace(ch rune) bool {
|
||||||
|
return ch == ' ' || ch == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isText(ch rune) bool {
|
||||||
|
return ch >= '!' && ch <= '~' && ch != equal && ch != l_brace && ch != r_brace
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEOF(ch rune) bool {
|
||||||
|
return ch == eof
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"gsock/gopher"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + T Y P E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
s *scanner
|
||||||
|
row int
|
||||||
|
buffer struct {
|
||||||
|
token Token
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Bookmarks gopher.Bookmarks
|
||||||
|
Colors []KeyValue
|
||||||
|
Settings []KeyValue
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyValue struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + R E C E I V E R S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
func (p *Parser) scan() (current Token) {
|
||||||
|
if p.buffer.size != 0 {
|
||||||
|
p.buffer.size = 0
|
||||||
|
return p.buffer.token
|
||||||
|
}
|
||||||
|
|
||||||
|
current = p.s.scan()
|
||||||
|
p.buffer.token = current
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseKeyValue() (KeyValue, error) {
|
||||||
|
kv := KeyValue{}
|
||||||
|
t1 := p.scan()
|
||||||
|
kv.Key = strings.TrimSpace(t1.val)
|
||||||
|
|
||||||
|
if t := p.scan(); t.kind == TOK_VALUE {
|
||||||
|
kv.Value = strings.TrimSpace(t.val)
|
||||||
|
} else {
|
||||||
|
return kv, fmt.Errorf("Got non-value expected VALUE on row %d", p.row)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t := p.scan(); t.kind != TOK_NEWLINE {
|
||||||
|
return kv, fmt.Errorf("Expected NEWLINE, got %q on row %d", t.kind, p.row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) unscan() { p.buffer.size = 1 }
|
||||||
|
|
||||||
|
|
||||||
|
func (p *Parser) Parse() (Config, error) {
|
||||||
|
p.row = 1
|
||||||
|
section := ""
|
||||||
|
c := Config{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if t := p.scan(); t.kind == TOK_NEWLINE {
|
||||||
|
p.row++
|
||||||
|
} else if t.kind == TOK_SECTION {
|
||||||
|
section = strings.ToUpper(t.val)
|
||||||
|
} else if t.kind == TOK_EOF {
|
||||||
|
break
|
||||||
|
} else if t.kind == TOK_KEY {
|
||||||
|
p.unscan()
|
||||||
|
keyval, err := p.parseKeyValue()
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
switch section {
|
||||||
|
case "BOOKMARKS":
|
||||||
|
c.Bookmarks.Add([]string{keyval.Value, keyval.Key})
|
||||||
|
case "COLORS":
|
||||||
|
c.Colors = append(c.Colors, keyval)
|
||||||
|
case "SETTINGS":
|
||||||
|
c.Settings = append(c.Settings, keyval)
|
||||||
|
}
|
||||||
|
} else if t.kind == TOK_ERROR {
|
||||||
|
return Config{}, fmt.Errorf("Error on row %d: %s", p.row, t.val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + F U N C T I O N S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
func NewParser(r io.Reader) *Parser {
|
||||||
|
return &Parser{s: NewScanner(r)}
|
||||||
|
}
|
176
cui/cui.go
176
cui/cui.go
|
@ -20,165 +20,6 @@ var shapes = map[string]string{
|
||||||
"scroll-track": "░",
|
"scroll-track": "░",
|
||||||
}
|
}
|
||||||
|
|
||||||
var screenInit bool = false
|
|
||||||
|
|
||||||
type Screen struct {
|
|
||||||
Height int
|
|
||||||
Width int
|
|
||||||
Windows []*Window
|
|
||||||
Activewindow int
|
|
||||||
}
|
|
||||||
|
|
||||||
type box struct {
|
|
||||||
row1 int
|
|
||||||
col1 int
|
|
||||||
row2 int
|
|
||||||
col2 int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Window struct {
|
|
||||||
Box box
|
|
||||||
Scrollbar bool
|
|
||||||
Scrollposition int
|
|
||||||
Content []string
|
|
||||||
drawBox bool
|
|
||||||
Active bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Screen) AddWindow(r1, c1, r2, c2 int, scroll, border bool) {
|
|
||||||
w := Window{box{r1, c1, r2, c2}, scroll, 0, []string{}, border, false}
|
|
||||||
s.Windows = append(s.Windows, &w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Screen) DrawFullScreen() {
|
|
||||||
s.Clear()
|
|
||||||
// w := s.Windows[s.Activewindow]
|
|
||||||
for _, w := range s.Windows {
|
|
||||||
if w.drawBox {
|
|
||||||
w.DrawBox()
|
|
||||||
}
|
|
||||||
|
|
||||||
w.DrawContent()
|
|
||||||
}
|
|
||||||
MoveCursorTo(s.Height - 1, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Screen) Clear() {
|
|
||||||
fill := strings.Repeat(" ", s.Width)
|
|
||||||
for i := 0; i <= s.Height; i++ {
|
|
||||||
MoveCursorTo(i, 0)
|
|
||||||
fmt.Print(fill)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Screen) SetCharMode() {
|
|
||||||
exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
|
|
||||||
exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
|
|
||||||
fmt.Print("\033[?25l")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks for a screen resize and resizes windows if needed
|
|
||||||
// Then redraws the screen. Takes a bool to decide whether
|
|
||||||
// to redraw the full screen or just the content. On a resize
|
|
||||||
// event, the full screen will always be redrawn.
|
|
||||||
func (s *Screen) ReflashScreen(clearScreen bool) {
|
|
||||||
oldh, oldw := s.Height, s.Width
|
|
||||||
s.GetSize()
|
|
||||||
if s.Height != oldh || s.Width != oldw {
|
|
||||||
for _, w := range s.Windows {
|
|
||||||
w.Box.row2 = s.Height - 2
|
|
||||||
w.Box.col2 = s.Width
|
|
||||||
}
|
|
||||||
s.DrawFullScreen()
|
|
||||||
} else if clearScreen {
|
|
||||||
s.DrawFullScreen()
|
|
||||||
} else {
|
|
||||||
s.Windows[s.Activewindow].DrawContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (s *Screen) GetSize() {
|
|
||||||
cmd := exec.Command("stty", "size")
|
|
||||||
cmd.Stdin = os.Stdin
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Fatal error: Unable to retrieve terminal size")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
var h, w int
|
|
||||||
fmt.Sscan(string(out), &h, &w)
|
|
||||||
s.Height = h
|
|
||||||
s.Width = w
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Window) DrawBox(){
|
|
||||||
moveThenDrawShape(w.Box.row1, w.Box.col1, "tl")
|
|
||||||
moveThenDrawShape(w.Box.row1, w.Box.col2, "tr")
|
|
||||||
moveThenDrawShape(w.Box.row2, w.Box.col1, "bl")
|
|
||||||
moveThenDrawShape(w.Box.row2, w.Box.col2, "br")
|
|
||||||
for i := w.Box.col1 + 1; i < w.Box.col2; i++ {
|
|
||||||
moveThenDrawShape(w.Box.row1, i, "ceiling")
|
|
||||||
moveThenDrawShape(w.Box.row2, i, "ceiling")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i:= w.Box.row1 + 1; i < w.Box.row2; i++ {
|
|
||||||
moveThenDrawShape(i, w.Box.col1, "wall")
|
|
||||||
moveThenDrawShape(i, w.Box.col2, "wall")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Window) DrawContent(){
|
|
||||||
var maxlines, borderw, contenth int
|
|
||||||
if w.drawBox {
|
|
||||||
borderw, contenth = -1, 1
|
|
||||||
} else {
|
|
||||||
borderw, contenth = 1, 0
|
|
||||||
}
|
|
||||||
height, width := w.Box.row2 - w.Box.row1 + borderw, w.Box.col2 - w.Box.col1 + borderw
|
|
||||||
content := WrapLines(w.Content, width)
|
|
||||||
if len(content) < w.Scrollposition + height {
|
|
||||||
maxlines = len(content)
|
|
||||||
} else {
|
|
||||||
maxlines = w.Scrollposition + height
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := w.Scrollposition; i < maxlines; i++ {
|
|
||||||
MoveCursorTo(w.Box.row1 + contenth + i - w.Scrollposition, w.Box.col1 + contenth)
|
|
||||||
fmt.Print( strings.Repeat(" ", width) )
|
|
||||||
MoveCursorTo(w.Box.row1 + contenth + i - w.Scrollposition, w.Box.col1 + contenth)
|
|
||||||
fmt.Print(content[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Window) ScrollDown() {
|
|
||||||
height := w.Box.row2 - w.Box.row1 - 1
|
|
||||||
contentLength := len(w.Content)
|
|
||||||
if w.Scrollposition < contentLength - height {
|
|
||||||
w.Scrollposition++
|
|
||||||
} else {
|
|
||||||
fmt.Print("\a")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Window) ScrollUp() {
|
|
||||||
if w.Scrollposition > 0 {
|
|
||||||
w.Scrollposition--
|
|
||||||
} else {
|
|
||||||
fmt.Print("\a")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//--------------------------------------------------------------------------//
|
|
||||||
// //
|
|
||||||
// F U N C T I O N S //
|
|
||||||
// //
|
|
||||||
//--------------------------------------------------------------------------//
|
|
||||||
|
|
||||||
|
|
||||||
func drawShape(shape string) {
|
func drawShape(shape string) {
|
||||||
if val, ok := shapes[shape]; ok {
|
if val, ok := shapes[shape]; ok {
|
||||||
fmt.Printf("%s", val)
|
fmt.Printf("%s", val)
|
||||||
|
@ -267,23 +108,6 @@ func WrapLines(s []string, length int) []string {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func NewScreen() *Screen {
|
|
||||||
if screenInit {
|
|
||||||
fmt.Println("Fatal error: Cannot create multiple screens")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
var s Screen
|
|
||||||
s.GetSize()
|
|
||||||
for i := 0; i < s.Height; i++ {
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
SetCharMode()
|
|
||||||
Clear("screen")
|
|
||||||
screenInit = true
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
func Getch() rune {
|
func Getch() rune {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
char, _, err := reader.ReadRune()
|
char, _, err := reader.ReadRune()
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package cui
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type MsgBar struct {
|
||||||
|
row int
|
||||||
|
title string
|
||||||
|
message string
|
||||||
|
showTitle bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MsgBar) SetTitle(s string) {
|
||||||
|
m.title = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MsgBar) SetMessage(s string) {
|
||||||
|
m.message = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MsgBar) ClearAll() {
|
||||||
|
MoveCursorTo(m.row, 1)
|
||||||
|
Clear("line")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MsgBar) ClearMessage() {
|
||||||
|
MoveCursorTo(m.row, len(m.title) + 1)
|
||||||
|
Clear("right")
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package cui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var screenInit bool = false
|
||||||
|
|
||||||
|
type Screen struct {
|
||||||
|
Height int
|
||||||
|
Width int
|
||||||
|
Windows []*Window
|
||||||
|
Activewindow int
|
||||||
|
Bars []*MsgBar
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (s *Screen) AddWindow(r1, c1, r2, c2 int, scroll, border, show bool) {
|
||||||
|
w := Window{box{r1, c1, r2, c2}, scroll, 0, []string{}, border, false, show}
|
||||||
|
s.Windows = append(s.Windows, &w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screen) AddMsgBar(row int, title, msg string, showTitle bool) {
|
||||||
|
b := MsgBar{row, title, msg, showTitle}
|
||||||
|
s.Bars = append(s.Bars, &b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Screen) DrawAllWindows() {
|
||||||
|
s.Clear()
|
||||||
|
for _, w := range s.Windows {
|
||||||
|
if w.Show {
|
||||||
|
w.DrawWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MoveCursorTo(s.Height - 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Screen) Clear() {
|
||||||
|
fill := strings.Repeat(" ", s.Width)
|
||||||
|
for i := 0; i <= s.Height; i++ {
|
||||||
|
MoveCursorTo(i, 0)
|
||||||
|
fmt.Print(fill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Screen) SetCharMode() {
|
||||||
|
exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
|
||||||
|
exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
|
||||||
|
fmt.Print("\033[?25l")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks for a screen resize and resizes windows if needed
|
||||||
|
// Then redraws the screen. Takes a bool to decide whether
|
||||||
|
// to redraw the full screen or just the content. On a resize
|
||||||
|
// event, the full screen will always be redrawn.
|
||||||
|
func (s *Screen) ReflashScreen(clearScreen bool) {
|
||||||
|
oldh, oldw := s.Height, s.Width
|
||||||
|
s.GetSize()
|
||||||
|
if s.Height != oldh || s.Width != oldw {
|
||||||
|
// TODO this should be pure library code and not rely on
|
||||||
|
// specific windows being present with specific behaviors.
|
||||||
|
// Maybe allow windows to have a resize function that can
|
||||||
|
// be declared within the application?
|
||||||
|
// For now this will be ok though.
|
||||||
|
s.Windows[0].Box.row2 = s.Height - 2
|
||||||
|
s.Windows[0].Box.col2 = s.Width
|
||||||
|
bookmarksWidth := 40
|
||||||
|
if s.Width < 40 {
|
||||||
|
bookmarksWidth = s.Width
|
||||||
|
}
|
||||||
|
s.Windows[1].Box.row2 = s.Height - 2
|
||||||
|
s.Windows[1].Box.col1 = s.Width - bookmarksWidth
|
||||||
|
s.Windows[1].Box.col2 = s.Width
|
||||||
|
|
||||||
|
s.DrawAllWindows()
|
||||||
|
s.DrawMsgBars()
|
||||||
|
} else if clearScreen {
|
||||||
|
s.DrawAllWindows()
|
||||||
|
s.DrawMsgBars()
|
||||||
|
} else {
|
||||||
|
for _, w := range s.Windows {
|
||||||
|
if w.Show {
|
||||||
|
w.DrawWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screen) DrawMsgBars() {
|
||||||
|
for _, bar := range s.Bars {
|
||||||
|
MoveCursorTo(bar.row, 1)
|
||||||
|
Clear("line")
|
||||||
|
fmt.Print("\033[7m")
|
||||||
|
fmt.Print(strings.Repeat(" ", s.Width))
|
||||||
|
MoveCursorTo(bar.row, 1)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
title := bar.title
|
||||||
|
fmt.Print(title)
|
||||||
|
if len(bar.title) > s.Width {
|
||||||
|
title = string(bar.title[:s.Width - 3]) + "..."
|
||||||
|
}
|
||||||
|
_, _ = buf.WriteString(title)
|
||||||
|
msg := bar.message
|
||||||
|
if len(bar.message) > s.Width - len(title) {
|
||||||
|
msg = string(bar.message[:s.Width - len(title) - 3]) + "..."
|
||||||
|
}
|
||||||
|
_, _ = buf.WriteString(msg)
|
||||||
|
|
||||||
|
MoveCursorTo(bar.row, 1)
|
||||||
|
fmt.Print(buf.String())
|
||||||
|
fmt.Print("\033[0m")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screen) GetSize() {
|
||||||
|
cmd := exec.Command("stty", "size")
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Fatal error: Unable to retrieve terminal size")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
var h, w int
|
||||||
|
fmt.Sscan(string(out), &h, &w)
|
||||||
|
s.Height = h
|
||||||
|
s.Width = w
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
|
||||||
|
func NewScreen() *Screen {
|
||||||
|
if screenInit {
|
||||||
|
fmt.Println("Fatal error: Cannot create multiple screens")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
var s Screen
|
||||||
|
s.GetSize()
|
||||||
|
for i := 0; i < s.Height; i++ {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
SetCharMode()
|
||||||
|
Clear("screen")
|
||||||
|
screenInit = true
|
||||||
|
return &s
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package cui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type box struct {
|
||||||
|
row1 int
|
||||||
|
col1 int
|
||||||
|
row2 int
|
||||||
|
col2 int
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add coloring
|
||||||
|
type Window struct {
|
||||||
|
Box box
|
||||||
|
Scrollbar bool
|
||||||
|
Scrollposition int
|
||||||
|
Content []string
|
||||||
|
drawBox bool
|
||||||
|
Active bool
|
||||||
|
Show bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Window) DrawWindow() {
|
||||||
|
w.DrawContent()
|
||||||
|
|
||||||
|
if w.drawBox {
|
||||||
|
w.DrawBox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Window) DrawBox(){
|
||||||
|
moveThenDrawShape(w.Box.row1, w.Box.col1, "tl")
|
||||||
|
moveThenDrawShape(w.Box.row1, w.Box.col2, "tr")
|
||||||
|
moveThenDrawShape(w.Box.row2, w.Box.col1, "bl")
|
||||||
|
moveThenDrawShape(w.Box.row2, w.Box.col2, "br")
|
||||||
|
for i := w.Box.col1 + 1; i < w.Box.col2; i++ {
|
||||||
|
moveThenDrawShape(w.Box.row1, i, "ceiling")
|
||||||
|
moveThenDrawShape(w.Box.row2, i, "ceiling")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i:= w.Box.row1 + 1; i < w.Box.row2; i++ {
|
||||||
|
moveThenDrawShape(i, w.Box.col1, "wall")
|
||||||
|
moveThenDrawShape(i, w.Box.col2, "wall")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Window) DrawContent(){
|
||||||
|
var maxlines, border_thickness, contenth int
|
||||||
|
var short_content bool = false
|
||||||
|
|
||||||
|
if w.drawBox {
|
||||||
|
border_thickness, contenth = -1, 1
|
||||||
|
} else {
|
||||||
|
border_thickness, contenth = 1, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
height := w.Box.row2 - w.Box.row1 + border_thickness
|
||||||
|
width := w.Box.col2 - w.Box.col1 + border_thickness
|
||||||
|
|
||||||
|
content := WrapLines(w.Content, width)
|
||||||
|
|
||||||
|
if len(content) < w.Scrollposition + height {
|
||||||
|
maxlines = len(content)
|
||||||
|
short_content = true
|
||||||
|
} else {
|
||||||
|
maxlines = w.Scrollposition + height
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := w.Scrollposition; i < maxlines; i++ {
|
||||||
|
MoveCursorTo(w.Box.row1 + contenth + i - w.Scrollposition, w.Box.col1 + contenth)
|
||||||
|
fmt.Print( strings.Repeat(" ", width) )
|
||||||
|
MoveCursorTo(w.Box.row1 + contenth + i - w.Scrollposition, w.Box.col1 + contenth)
|
||||||
|
fmt.Print(content[i])
|
||||||
|
}
|
||||||
|
if short_content {
|
||||||
|
for i := len(content); i <= height; i++ {
|
||||||
|
MoveCursorTo(w.Box.row1 + contenth + i - w.Scrollposition, w.Box.col1 + contenth)
|
||||||
|
fmt.Print( strings.Repeat(" ", width) )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Window) ScrollDown() {
|
||||||
|
height := w.Box.row2 - w.Box.row1 - 1
|
||||||
|
contentLength := len(w.Content)
|
||||||
|
if w.Scrollposition < contentLength - height {
|
||||||
|
w.Scrollposition++
|
||||||
|
} else {
|
||||||
|
fmt.Print("\a")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Window) ScrollUp() {
|
||||||
|
if w.Scrollposition > 0 {
|
||||||
|
w.Scrollposition--
|
||||||
|
} else {
|
||||||
|
fmt.Print("\a")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
341
gclient.go
341
gclient.go
|
@ -1,28 +1,56 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"gsock/gopher"
|
"gsock/gopher"
|
||||||
"os"
|
"gsock/cmdparse"
|
||||||
|
"gsock/config"
|
||||||
|
"gsock/cui"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"gsock/cui"
|
|
||||||
"errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var history gopher.History = gopher.MakeHistory()
|
var history gopher.History = gopher.MakeHistory()
|
||||||
var screen *cui.Screen
|
var screen *cui.Screen
|
||||||
var userinfo, _ = user.Current()
|
var userinfo, _ = user.Current()
|
||||||
|
var settings config.Config
|
||||||
|
var options = map[string]string{
|
||||||
|
"homeurl": "",
|
||||||
|
"savelocation": userinfo.HomeDir + "/Downloads/",
|
||||||
|
"searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs",
|
||||||
|
"openhttp": "false",
|
||||||
|
"httpbrowser": "lynx",
|
||||||
|
}
|
||||||
|
|
||||||
func err_exit(err string, code int) {
|
func err_exit(err string, code int) {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func save_file() {
|
func save_file(address, name string) error {
|
||||||
//TODO add a way to save a file...
|
quickMessage("Saving file...", false)
|
||||||
//eg. :save 5 test.txt
|
defer quickMessage("Saving file...", true)
|
||||||
|
|
||||||
|
url, err := gopher.MakeUrl(address)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := gopher.Retrieve(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(userinfo.HomeDir + "/" + name, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func search(u string) error {
|
func search(u string) error {
|
||||||
|
@ -42,16 +70,93 @@ func search(u string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func route_input(s string) error {
|
func route_input(com *cmdparse.Command) error {
|
||||||
if num, _ := regexp.MatchString(`^\d+$`, s); num && history.Length > 0 {
|
var err error
|
||||||
linkcount := len(history.Collection[history.Position].Links)
|
switch com.Type {
|
||||||
item, _ := strconv.Atoi(s)
|
case cmdparse.SIMPLE:
|
||||||
if item <= linkcount {
|
err = simple_command(com.Action)
|
||||||
linkurl := history.Collection[history.Position].Links[item - 1]
|
case cmdparse.GOURL:
|
||||||
v, err := gopher.Visit(linkurl)
|
err = go_to_url(com.Target)
|
||||||
|
case cmdparse.GOLINK:
|
||||||
|
err = go_to_link(com.Target)
|
||||||
|
case cmdparse.DOLINK:
|
||||||
|
err = do_link_command(com.Action, com.Target)
|
||||||
|
case cmdparse.DOAS:
|
||||||
|
err = do_command_as(com.Action, com.Value)
|
||||||
|
case cmdparse.DOLINKAS:
|
||||||
|
err = do_link_command_as(com.Action, com.Target, com.Value)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown command entry!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggle_bookmarks() {
|
||||||
|
bookmarks := screen.Windows[1]
|
||||||
|
if bookmarks.Show {
|
||||||
|
bookmarks.Show = false
|
||||||
|
} else {
|
||||||
|
bookmarks.Show = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if screen.Activewindow == 0 {
|
||||||
|
screen.Activewindow = 1
|
||||||
|
} else {
|
||||||
|
screen.Activewindow = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func simple_command(a string) error {
|
||||||
|
a = strings.ToUpper(a)
|
||||||
|
switch a {
|
||||||
|
case "Q", "QUIT":
|
||||||
|
cui.Exit()
|
||||||
|
case "H", "HOME":
|
||||||
|
return go_home()
|
||||||
|
case "B", "BOOKMARKS":
|
||||||
|
toggle_bookmarks()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown action %q", a)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func go_to_url(u string) error {
|
||||||
|
quickMessage("Loading...", false)
|
||||||
|
v, err := gopher.Visit(u)
|
||||||
|
if err != nil {
|
||||||
|
quickMessage("Loading...", true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
quickMessage("Loading...", true)
|
||||||
|
|
||||||
|
if v.Address.Gophertype == "7" {
|
||||||
|
err := search(v.Address.Full)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if v.Address.IsBinary {
|
||||||
|
// TO DO: run this into the write to file method
|
||||||
|
} else {
|
||||||
|
history.Add(v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func go_to_link(l string) error {
|
||||||
|
if num, _ := regexp.MatchString(`^\d+$`, l); num && history.Length > 0 {
|
||||||
|
linkcount := len(history.Collection[history.Position].Links)
|
||||||
|
item, _ := strconv.Atoi(l)
|
||||||
|
if item <= linkcount {
|
||||||
|
linkurl := history.Collection[history.Position].Links[item - 1]
|
||||||
|
quickMessage("Loading...", false)
|
||||||
|
v, err := gopher.Visit(linkurl)
|
||||||
|
if err != nil {
|
||||||
|
quickMessage("Loading...", true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
quickMessage("Loading...", true)
|
||||||
|
|
||||||
if v.Address.Gophertype == "7" {
|
if v.Address.Gophertype == "7" {
|
||||||
err := search(linkurl)
|
err := search(linkurl)
|
||||||
|
@ -59,61 +164,200 @@ func route_input(s string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if v.Address.IsBinary {
|
} else if v.Address.IsBinary {
|
||||||
// TODO add download querying here
|
// TO DO: run this into the write to file method
|
||||||
} else {
|
} else {
|
||||||
history.Add(v)
|
history.Add(v)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errname := fmt.Sprintf("Invalid link id: %s", s)
|
return fmt.Errorf("Invalid link id: %s", l)
|
||||||
return errors.New(errname)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
v, err := gopher.Visit(s)
|
return fmt.Errorf("Invalid link id: %s", l)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if v.Address.Gophertype == "7" {
|
|
||||||
err := search(v.Address.Full)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if v.Address.IsBinary {
|
|
||||||
// TODO add download querying here
|
|
||||||
} else {
|
|
||||||
history.Add(v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func go_home() error {
|
||||||
|
if options["homeurl"] != "" {
|
||||||
|
return go_to_url(options["homeurl"])
|
||||||
|
}
|
||||||
|
return fmt.Errorf("No home address has been set")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func do_link_command(action, target string) error {
|
||||||
// fmt.Println(userinfo.HomeDir)
|
num, err := strconv.Atoi(target)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Expected number, got %q", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "DELETE", "D":
|
||||||
|
err := settings.Bookmarks.Del(num)
|
||||||
|
screen.Windows[1].Content = settings.Bookmarks.List()
|
||||||
|
save_config()
|
||||||
|
return err
|
||||||
|
case "BOOKMARKS", "B":
|
||||||
|
if num > len(settings.Bookmarks.Links) - 1 {
|
||||||
|
return fmt.Errorf("There is no bookmark with ID %d", num)
|
||||||
|
}
|
||||||
|
err := go_to_url(settings.Bookmarks.Links[num])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("This method has not been built")
|
||||||
|
}
|
||||||
|
|
||||||
|
func do_command_as(action string, values []string) error {
|
||||||
|
if len(values) < 2 {
|
||||||
|
return fmt.Errorf("%q", values)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "ADD", "A":
|
||||||
|
if values[0] == "." {
|
||||||
|
values[0] = history.Collection[history.Position].Address.Full
|
||||||
|
}
|
||||||
|
err := settings.Bookmarks.Add(values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
screen.Windows[1].Content = settings.Bookmarks.List()
|
||||||
|
save_config()
|
||||||
|
return nil
|
||||||
|
case "WRITE", "W":
|
||||||
|
return save_file(values[0], strings.Join(values[1:], " "))
|
||||||
|
case "SET", "S":
|
||||||
|
if _, ok := options[values[0]]; ok {
|
||||||
|
options[values[0]] = strings.Join(values[1:], " ")
|
||||||
|
save_config()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Unable to set %s, it does not exist",values[0])
|
||||||
|
}
|
||||||
|
return fmt.Errorf("This method has not been built")
|
||||||
|
}
|
||||||
|
|
||||||
|
func do_link_command_as(action, target string, values []string) error {
|
||||||
|
num, err := strconv.Atoi(target)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Expected number, got %q", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
links := history.Collection[history.Position].Links
|
||||||
|
if num >= len(links) {
|
||||||
|
return fmt.Errorf("Invalid link id: %s", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "ADD", "A":
|
||||||
|
newBookmark := append([]string{links[num - 1]}, values...)
|
||||||
|
err := settings.Bookmarks.Add(newBookmark)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
screen.Windows[1].Content = settings.Bookmarks.List()
|
||||||
|
save_config()
|
||||||
|
return nil
|
||||||
|
case "WRITE", "W":
|
||||||
|
return save_file(links[num - 1], strings.Join(values, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("This method has not been built")
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearInput(incError bool) {
|
||||||
|
cui.MoveCursorTo(screen.Height - 1, 0)
|
||||||
|
cui.Clear("line")
|
||||||
|
if incError {
|
||||||
|
cui.MoveCursorTo(screen.Height, 0)
|
||||||
|
cui.Clear("line")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func quickMessage(msg string, clearMsg bool) {
|
||||||
|
cui.MoveCursorTo(screen.Height, screen.Width - 2 - len(msg))
|
||||||
|
if clearMsg {
|
||||||
|
cui.Clear("right")
|
||||||
|
} else {
|
||||||
|
fmt.Print("\033[48;5;21m\033[38;5;15m", msg, "\033[0m")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save_config() {
|
||||||
|
bkmrks := settings.Bookmarks.IniDump()
|
||||||
|
opts := "\n[SETTINGS]\n"
|
||||||
|
for k, v := range options {
|
||||||
|
opts += k
|
||||||
|
opts += "="
|
||||||
|
opts += v
|
||||||
|
opts += "\n"
|
||||||
|
}
|
||||||
|
ioutil.WriteFile(userinfo.HomeDir + "/.badger.ini", []byte(bkmrks+opts), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func load_config() {
|
||||||
|
file, _ := os.Open(userinfo.HomeDir + "/.badger.ini")
|
||||||
|
confparser := config.NewParser(file)
|
||||||
|
settings, _ = confparser.Parse()
|
||||||
|
file.Close()
|
||||||
|
screen.Windows[1].Content = settings.Bookmarks.List()
|
||||||
|
for _, v := range settings.Settings {
|
||||||
|
lowerkey := strings.ToLower(v.Key)
|
||||||
|
if _, ok := options[lowerkey]; ok {
|
||||||
|
options[lowerkey] = v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initClient() {
|
||||||
history.Position = -1
|
history.Position = -1
|
||||||
screen = cui.NewScreen()
|
screen = cui.NewScreen()
|
||||||
screen.SetCharMode()
|
screen.SetCharMode()
|
||||||
|
screen.AddWindow(2, 1, screen.Height - 2, screen.Width, false, false, true)
|
||||||
|
screen.AddMsgBar(1, " ((( Badger ))) ", " A fun gopher client!", true)
|
||||||
|
bookmarksWidth := 40
|
||||||
|
if screen.Width < 40 {
|
||||||
|
bookmarksWidth = screen.Width
|
||||||
|
}
|
||||||
|
screen.AddWindow(2, screen.Width - bookmarksWidth, screen.Height - 2, screen.Width, false, true, false)
|
||||||
|
load_config()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
defer cui.Exit()
|
defer cui.Exit()
|
||||||
screen.AddWindow(1, 1, screen.Height - 2, screen.Width, false, false)
|
initClient()
|
||||||
mainWindow := screen.Windows[0]
|
mainWindow := screen.Windows[0]
|
||||||
|
first_load := true
|
||||||
|
|
||||||
redrawScreen := true
|
redrawScreen := true
|
||||||
|
|
||||||
for {
|
for {
|
||||||
screen.ReflashScreen(redrawScreen)
|
screen.ReflashScreen(redrawScreen)
|
||||||
|
|
||||||
|
if first_load {
|
||||||
|
go_home()
|
||||||
|
first_load = false
|
||||||
|
mainWindow.Content = history.Collection[history.Position].Content
|
||||||
|
screen.Bars[0].SetMessage(history.Collection[history.Position].Address.Full)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
redrawScreen = false
|
redrawScreen = false
|
||||||
|
|
||||||
c := cui.Getch()
|
c := cui.Getch()
|
||||||
switch c {
|
switch c {
|
||||||
case 'j', 'J':
|
case 'j', 'J':
|
||||||
mainWindow.ScrollDown()
|
screen.Windows[screen.Activewindow].ScrollDown()
|
||||||
case 'k', 'K':
|
case 'k', 'K':
|
||||||
mainWindow.ScrollUp()
|
screen.Windows[screen.Activewindow].ScrollUp()
|
||||||
case 'q', 'Q':
|
case 'q', 'Q':
|
||||||
cui.Exit()
|
cui.Exit()
|
||||||
case 'b', 'B':
|
case 'b':
|
||||||
history.GoBack()
|
history.GoBack()
|
||||||
mainWindow.Scrollposition = 0
|
mainWindow.Scrollposition = 0
|
||||||
redrawScreen = true
|
redrawScreen = true
|
||||||
|
case 'B':
|
||||||
|
toggle_bookmarks()
|
||||||
case 'f', 'F':
|
case 'f', 'F':
|
||||||
history.GoForward()
|
history.GoForward()
|
||||||
mainWindow.Scrollposition = 0
|
mainWindow.Scrollposition = 0
|
||||||
|
@ -122,28 +366,33 @@ func main() {
|
||||||
redrawScreen = true
|
redrawScreen = true
|
||||||
cui.MoveCursorTo(screen.Height - 1, 0)
|
cui.MoveCursorTo(screen.Height - 1, 0)
|
||||||
entry := cui.GetLine()
|
entry := cui.GetLine()
|
||||||
// Clear entry line
|
// Clear entry line and error line
|
||||||
cui.MoveCursorTo(screen.Height - 1, 0)
|
clearInput(true)
|
||||||
cui.Clear("line")
|
|
||||||
if entry == "" {
|
if entry == "" {
|
||||||
cui.MoveCursorTo(screen.Height - 1, 0)
|
redrawScreen = false
|
||||||
fmt.Print(" ")
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err := route_input(entry)
|
parser := cmdparse.NewParser(strings.NewReader(entry))
|
||||||
|
p, err := parser.Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Display error
|
|
||||||
cui.MoveCursorTo(screen.Height, 0)
|
cui.MoveCursorTo(screen.Height, 0)
|
||||||
fmt.Print("\033[41m\033[37m", err, "\033[0m")
|
fmt.Print("\033[41m\033[37m", err, "\033[0m")
|
||||||
// Set screen to not reflash
|
// Set screen to not reflash
|
||||||
redrawScreen = false
|
redrawScreen = false
|
||||||
} else {
|
} else {
|
||||||
mainWindow.Scrollposition = 0
|
err := route_input(p)
|
||||||
// screen.Clear()
|
if err != nil {
|
||||||
|
cui.MoveCursorTo(screen.Height, 0)
|
||||||
|
fmt.Print("\033[41m\033[37m", err, "\033[0m")
|
||||||
|
redrawScreen = false
|
||||||
|
} else {
|
||||||
|
mainWindow.Scrollposition = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if history.Position >= 0 {
|
if history.Position >= 0 {
|
||||||
mainWindow.Content = history.Collection[history.Position].Content
|
mainWindow.Content = history.Collection[history.Position].Content
|
||||||
|
screen.Bars[0].SetMessage(history.Collection[history.Position].Address.Full)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package gopher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bookmarks struct {
|
||||||
|
Titles []string
|
||||||
|
Links []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bookmarks) Add(v []string) error {
|
||||||
|
if len(v) < 2 {
|
||||||
|
return fmt.Errorf("Received %d arguments, expected 2 or more", len(v))
|
||||||
|
}
|
||||||
|
b.Titles = append(b.Titles, strings.Join(v[1:], " "))
|
||||||
|
b.Links = append(b.Links, v[0])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bookmarks) Del(i int) error {
|
||||||
|
if i < len(b.Titles) && i < len(b.Links) {
|
||||||
|
b.Titles = append(b.Titles[:i], b.Titles[i + 1:]...)
|
||||||
|
b.Links = append(b.Links[:i], b.Links[i + 1:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Bookmark %d does not exist", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Bookmarks) List() []string {
|
||||||
|
var out []string
|
||||||
|
for i, t := range b.Titles {
|
||||||
|
out = append(out, fmt.Sprintf("[%d] %s", i, t))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (b Bookmarks) IniDump() string {
|
||||||
|
if len(b.Titles) < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
out := "[BOOKMARKS]\n"
|
||||||
|
for i := 0; i < len(b.Titles); i++ {
|
||||||
|
out += b.Titles[i]
|
||||||
|
out += "="
|
||||||
|
out += b.Links[i]
|
||||||
|
out += "\n"
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func MakeBookmarks() Bookmarks {
|
||||||
|
return Bookmarks{[]string{}, []string{}}
|
||||||
|
}
|
241
gopher/gopher.go
241
gopher/gopher.go
|
@ -4,56 +4,14 @@
|
||||||
package gopher
|
package gopher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
|
||||||
"net"
|
"net"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
//------------------------------------------------\\
|
|
||||||
// + + + T Y P E S + + + \\
|
|
||||||
//--------------------------------------------------\\
|
|
||||||
|
|
||||||
// The history struct represents the history of the browsing
|
|
||||||
// session. It contains the current history position, the
|
|
||||||
// length of the active history space (this can be different
|
|
||||||
// from the available capacity in the Collection), and a
|
|
||||||
// collection array containing View structs representing
|
|
||||||
// each page in the current history. In general usage this
|
|
||||||
// struct should be initialized via the MakeHistory function.
|
|
||||||
type History struct {
|
|
||||||
Position int
|
|
||||||
Length int
|
|
||||||
Collection [20]View
|
|
||||||
}
|
|
||||||
|
|
||||||
// The view struct represents a gopher page. It contains
|
|
||||||
// the page content as a string slice, a list of link URLs
|
|
||||||
// as string slices, and the Url struct representing the page.
|
|
||||||
type View struct {
|
|
||||||
Content []string
|
|
||||||
Links []string
|
|
||||||
Address Url
|
|
||||||
}
|
|
||||||
|
|
||||||
// The url struct represents a URL for the rest of the system.
|
|
||||||
// It includes component parts as well as a full URL string.
|
|
||||||
type Url struct {
|
|
||||||
Scheme string
|
|
||||||
Host string
|
|
||||||
Port string
|
|
||||||
Gophertype string
|
|
||||||
Resource string
|
|
||||||
Full string
|
|
||||||
IsBinary bool
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//------------------------------------------------\\
|
//------------------------------------------------\\
|
||||||
// + + + V A R I A B L E S + + + \\
|
// + + + V A R I A B L E S + + + \\
|
||||||
//--------------------------------------------------\\
|
//--------------------------------------------------\\
|
||||||
|
@ -77,210 +35,11 @@ var types = map[string]string{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//------------------------------------------------\\
|
|
||||||
// + + + R E C E I V E R S + + + \\
|
|
||||||
//--------------------------------------------------\\
|
|
||||||
|
|
||||||
// The "Add" receiver takes a view and adds it to
|
|
||||||
// the history struct that called it. "Add" returns
|
|
||||||
// nothing. "Add" will shift history down if the max
|
|
||||||
// history length would be exceeded, and will reset
|
|
||||||
// history length if something is added in the middle.
|
|
||||||
func (h *History) Add(v View) {
|
|
||||||
v.ParseMap()
|
|
||||||
if h.Position == h.Length - 1 && h.Length < len(h.Collection) {
|
|
||||||
h.Collection[h.Length] = v
|
|
||||||
h.Length++
|
|
||||||
h.Position++
|
|
||||||
} else if h.Position == h.Length - 1 && h.Length == 20 {
|
|
||||||
for x := 1; x < len(h.Collection); x++ {
|
|
||||||
h.Collection[x-1] = h.Collection[x]
|
|
||||||
}
|
|
||||||
h.Collection[len(h.Collection)-1] = v
|
|
||||||
} else {
|
|
||||||
h.Position += 1
|
|
||||||
h.Length = h.Position + 1
|
|
||||||
h.Collection[h.Position] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The "Get" receiver is called by a history struct
|
|
||||||
// and returns a View from the current position, will
|
|
||||||
// return an error if history is empty and there is
|
|
||||||
// nothing to get.
|
|
||||||
func (h History) Get() (*View, error) {
|
|
||||||
if h.Position < 0 {
|
|
||||||
return nil, errors.New("History is empty, cannot get item from empty history.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &h.Collection[h.Position], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The "GoBack" receiver is called by a history struct.
|
|
||||||
// When called it decrements the current position and
|
|
||||||
// displays the content for the View in that position.
|
|
||||||
// If history is at position 0, no action is taken.
|
|
||||||
func (h *History) GoBack() {
|
|
||||||
if h.Position > 0 {
|
|
||||||
h.Position--
|
|
||||||
} else {
|
|
||||||
fmt.Print("\a")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// The "GoForward" receiver is called by a history struct.
|
|
||||||
// When called it increments the current position and
|
|
||||||
// displays the content for the View in that position.
|
|
||||||
// If history is at position len - 1, no action is taken.
|
|
||||||
func (h *History) GoForward() {
|
|
||||||
if h.Position + 1 < h.Length {
|
|
||||||
h.Position++
|
|
||||||
h.DisplayCurrentView()
|
|
||||||
} else {
|
|
||||||
fmt.Print("\a")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The "DisplayCurrentView" receiver is called by a history
|
|
||||||
// struct. It calls the Display receiver for th view struct
|
|
||||||
// at the current history position. "DisplayCurrentView" does
|
|
||||||
// not return anything, and does nothing if position is less
|
|
||||||
// that 0.
|
|
||||||
func (h *History) DisplayCurrentView() {
|
|
||||||
h.Collection[h.Position].Display()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// The "ParseMap" receiver is called by a view struct. It
|
|
||||||
// checks if the view is for a gophermap. If not,it does
|
|
||||||
// nothing. If so, it parses the gophermap into comment lines
|
|
||||||
// and link lines. For link lines it adds a link to the links
|
|
||||||
// slice and changes the content value to just the printable
|
|
||||||
// string plus a gophertype indicator and a link number that
|
|
||||||
// relates to the link position in the links slice. This
|
|
||||||
// receiver does not return anything.
|
|
||||||
func (v *View) ParseMap() {
|
|
||||||
if v.Address.Gophertype == "1" || v.Address.Gophertype == "7" {
|
|
||||||
for i, e := range v.Content {
|
|
||||||
e = strings.Trim(e, "\r\n")
|
|
||||||
line := strings.Split(e,"\t")
|
|
||||||
var title string
|
|
||||||
if len(line[0]) > 1 {
|
|
||||||
title = line[0][1:]
|
|
||||||
} else {
|
|
||||||
title = ""
|
|
||||||
}
|
|
||||||
if len(line[0]) > 0 && string(line[0][0]) == "i" {
|
|
||||||
v.Content[i] = " " + string(title)
|
|
||||||
continue
|
|
||||||
} else if len(line) >= 4 {
|
|
||||||
fulllink := fmt.Sprintf("%s:%s/%s%s", line[2], line[3], string(line[0][0]), line[1])
|
|
||||||
v.Links = append(v.Links, fulllink)
|
|
||||||
linktext := fmt.Sprintf("(%s) %2d %s", GetType(string(line[0][0])), len(v.Links), title)
|
|
||||||
v.Content[i] = linktext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The "Display" receiver is called on a view struct.
|
|
||||||
// It prints the content, line by line, of the View.
|
|
||||||
// This receiver does not return anything.
|
|
||||||
func (v View) Display() {
|
|
||||||
fmt.Println()
|
|
||||||
for _, el := range v.Content {
|
|
||||||
fmt.Println(el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//------------------------------------------------\\
|
//------------------------------------------------\\
|
||||||
// + + + F U N C T I O N S + + + \\
|
// + + + F U N C T I O N S + + + \\
|
||||||
//--------------------------------------------------\\
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
// MakeUrl is a Url constructor that takes in a string
|
|
||||||
// representation of a url and returns a Url struct and
|
|
||||||
// an error (or nil).
|
|
||||||
func MakeUrl(u string) (Url, error) {
|
|
||||||
var out Url
|
|
||||||
re := regexp.MustCompile(`^((?P<scheme>gopher|http|https|ftp|telnet):\/\/)?(?P<host>[\w\-\.\d]+)(?::(?P<port>\d+)?)?(?:/(?P<type>[01345679gIhisp])?)?(?P<resource>(?:\/.*)?)?$`)
|
|
||||||
match := re.FindStringSubmatch(u)
|
|
||||||
|
|
||||||
if valid := re.MatchString(u); valid != true {
|
|
||||||
return out, errors.New("Invalid URL or command character")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, name := range re.SubexpNames() {
|
|
||||||
switch name {
|
|
||||||
case "scheme":
|
|
||||||
out.Scheme = match[i]
|
|
||||||
case "host":
|
|
||||||
out.Host = match[i]
|
|
||||||
case "port":
|
|
||||||
out.Port = match[i]
|
|
||||||
case "type":
|
|
||||||
out.Gophertype = match[i]
|
|
||||||
case "resource":
|
|
||||||
out.Resource = match[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if out.Scheme == "" {
|
|
||||||
out.Scheme = "gopher"
|
|
||||||
}
|
|
||||||
|
|
||||||
if out.Host == "" {
|
|
||||||
return out, errors.New("No host.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if out.Scheme == "gopher" && out.Port == "" {
|
|
||||||
out.Port = "70"
|
|
||||||
} else if out.Scheme == "http" || out.Scheme == "https" && out.Port == "" {
|
|
||||||
out.Port = "80"
|
|
||||||
}
|
|
||||||
|
|
||||||
if out.Gophertype == "" && (out.Resource == "" || out.Resource == "/") {
|
|
||||||
out.Gophertype = "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if out.Gophertype == "1" || out.Gophertype == "0" {
|
|
||||||
out.IsBinary = false
|
|
||||||
} else {
|
|
||||||
out.IsBinary = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if out.Scheme == "gopher" && out.Gophertype == "" {
|
|
||||||
out.Gophertype = "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
out.Full = out.Scheme + "://" + out.Host + ":" + out.Port + "/" + out.Gophertype + out.Resource
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Constructor function for History struct.
|
|
||||||
// This is used to initialize history position
|
|
||||||
// as -1, which is needed. Returns a copy of
|
|
||||||
// initialized History struct (does NOT return
|
|
||||||
// a pointer to the struct).
|
|
||||||
func MakeHistory() History {
|
|
||||||
return History{-1, 0, [20]View{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Constructor function for View struct.
|
|
||||||
// This is used to initialize a View with
|
|
||||||
// a Url struct, links, and content. It takes
|
|
||||||
// a Url struct and a content []string and returns
|
|
||||||
// a View (NOT a pointer to a View).
|
|
||||||
func MakeView(url Url, content []string) View {
|
|
||||||
v := View{content, make([]string, 0), url}
|
|
||||||
v.ParseMap()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Retrieve makes a request to a Url and resturns
|
// Retrieve makes a request to a Url and resturns
|
||||||
// the response as []byte/error. This function is
|
// the response as []byte/error. This function is
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
package gopher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + T Y P E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
// The history struct represents the history of the browsing
|
||||||
|
// session. It contains the current history position, the
|
||||||
|
// length of the active history space (this can be different
|
||||||
|
// from the available capacity in the Collection), and a
|
||||||
|
// collection array containing View structs representing
|
||||||
|
// each page in the current history. In general usage this
|
||||||
|
// struct should be initialized via the MakeHistory function.
|
||||||
|
type History struct {
|
||||||
|
Position int
|
||||||
|
Length int
|
||||||
|
Collection [20]View
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + R E C E I V E R S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
// The "Add" receiver takes a view and adds it to
|
||||||
|
// the history struct that called it. "Add" returns
|
||||||
|
// nothing. "Add" will shift history down if the max
|
||||||
|
// history length would be exceeded, and will reset
|
||||||
|
// history length if something is added in the middle.
|
||||||
|
func (h *History) Add(v View) {
|
||||||
|
v.ParseMap()
|
||||||
|
if h.Position == h.Length - 1 && h.Length < len(h.Collection) {
|
||||||
|
h.Collection[h.Length] = v
|
||||||
|
h.Length++
|
||||||
|
h.Position++
|
||||||
|
} else if h.Position == h.Length - 1 && h.Length == 20 {
|
||||||
|
for x := 1; x < len(h.Collection); x++ {
|
||||||
|
h.Collection[x-1] = h.Collection[x]
|
||||||
|
}
|
||||||
|
h.Collection[len(h.Collection)-1] = v
|
||||||
|
} else {
|
||||||
|
h.Position += 1
|
||||||
|
h.Length = h.Position + 1
|
||||||
|
h.Collection[h.Position] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "Get" receiver is called by a history struct
|
||||||
|
// and returns a View from the current position, will
|
||||||
|
// return an error if history is empty and there is
|
||||||
|
// nothing to get.
|
||||||
|
func (h History) Get() (*View, error) {
|
||||||
|
if h.Position < 0 {
|
||||||
|
return nil, errors.New("History is empty, cannot get item from empty history.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &h.Collection[h.Position], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "GoBack" receiver is called by a history struct.
|
||||||
|
// When called it decrements the current position and
|
||||||
|
// displays the content for the View in that position.
|
||||||
|
// If history is at position 0, no action is taken.
|
||||||
|
func (h *History) GoBack() {
|
||||||
|
if h.Position > 0 {
|
||||||
|
h.Position--
|
||||||
|
} else {
|
||||||
|
fmt.Print("\a")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The "GoForward" receiver is called by a history struct.
|
||||||
|
// When called it increments the current position and
|
||||||
|
// displays the content for the View in that position.
|
||||||
|
// If history is at position len - 1, no action is taken.
|
||||||
|
func (h *History) GoForward() {
|
||||||
|
if h.Position + 1 < h.Length {
|
||||||
|
h.Position++
|
||||||
|
h.DisplayCurrentView()
|
||||||
|
} else {
|
||||||
|
fmt.Print("\a")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "DisplayCurrentView" receiver is called by a history
|
||||||
|
// struct. It calls the Display receiver for th view struct
|
||||||
|
// at the current history position. "DisplayCurrentView" does
|
||||||
|
// not return anything, and does nothing if position is less
|
||||||
|
// that 0.
|
||||||
|
func (h *History) DisplayCurrentView() {
|
||||||
|
h.Collection[h.Position].Display()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + F U N C T I O N S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
|
||||||
|
// Constructor function for History struct.
|
||||||
|
// This is used to initialize history position
|
||||||
|
// as -1, which is needed. Returns a copy of
|
||||||
|
// initialized History struct (does NOT return
|
||||||
|
// a pointer to the struct).
|
||||||
|
func MakeHistory() History {
|
||||||
|
return History{-1, 0, [20]View{}}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
package gopher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + T Y P E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
|
||||||
|
// The url struct represents a URL for the rest of the system.
|
||||||
|
// It includes component parts as well as a full URL string.
|
||||||
|
type Url struct {
|
||||||
|
Scheme string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Gophertype string
|
||||||
|
Resource string
|
||||||
|
Full string
|
||||||
|
IsBinary bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + F U N C T I O N S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
// MakeUrl is a Url constructor that takes in a string
|
||||||
|
// representation of a url and returns a Url struct and
|
||||||
|
// an error (or nil).
|
||||||
|
func MakeUrl(u string) (Url, error) {
|
||||||
|
var out Url
|
||||||
|
re := regexp.MustCompile(`^((?P<scheme>gopher|http|https|ftp|telnet):\/\/)?(?P<host>[\w\-\.\d]+)(?::(?P<port>\d+)?)?(?:/(?P<type>[01345679gIhisp])?)?(?P<resource>(?:\/.*)?)?$`)
|
||||||
|
match := re.FindStringSubmatch(u)
|
||||||
|
|
||||||
|
if valid := re.MatchString(u); valid != true {
|
||||||
|
return out, errors.New("Invalid URL or command character")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, name := range re.SubexpNames() {
|
||||||
|
switch name {
|
||||||
|
case "scheme":
|
||||||
|
out.Scheme = match[i]
|
||||||
|
case "host":
|
||||||
|
out.Host = match[i]
|
||||||
|
case "port":
|
||||||
|
out.Port = match[i]
|
||||||
|
case "type":
|
||||||
|
out.Gophertype = match[i]
|
||||||
|
case "resource":
|
||||||
|
out.Resource = match[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Scheme == "" {
|
||||||
|
out.Scheme = "gopher"
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Host == "" {
|
||||||
|
return out, errors.New("No host.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Scheme == "gopher" && out.Port == "" {
|
||||||
|
out.Port = "70"
|
||||||
|
} else if out.Scheme == "http" || out.Scheme == "https" && out.Port == "" {
|
||||||
|
out.Port = "80"
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Gophertype == "" && (out.Resource == "" || out.Resource == "/") {
|
||||||
|
out.Gophertype = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Gophertype == "1" || out.Gophertype == "0" {
|
||||||
|
out.IsBinary = false
|
||||||
|
} else {
|
||||||
|
out.IsBinary = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Scheme == "gopher" && out.Gophertype == "" {
|
||||||
|
out.Gophertype = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Full = out.Scheme + "://" + out.Host + ":" + out.Port + "/" + out.Gophertype + out.Resource
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package gopher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + T Y P E S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
|
||||||
|
// The view struct represents a gopher page. It contains
|
||||||
|
// the page content as a string slice, a list of link URLs
|
||||||
|
// as string slices, and the Url struct representing the page.
|
||||||
|
type View struct {
|
||||||
|
Content []string
|
||||||
|
Links []string
|
||||||
|
Address Url
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + R E C E I V E R S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
|
||||||
|
// The "ParseMap" receiver is called by a view struct. It
|
||||||
|
// checks if the view is for a gophermap. If not,it does
|
||||||
|
// nothing. If so, it parses the gophermap into comment lines
|
||||||
|
// and link lines. For link lines it adds a link to the links
|
||||||
|
// slice and changes the content value to just the printable
|
||||||
|
// string plus a gophertype indicator and a link number that
|
||||||
|
// relates to the link position in the links slice. This
|
||||||
|
// receiver does not return anything.
|
||||||
|
func (v *View) ParseMap() {
|
||||||
|
if v.Address.Gophertype == "1" || v.Address.Gophertype == "7" {
|
||||||
|
for i, e := range v.Content {
|
||||||
|
e = strings.Trim(e, "\r\n")
|
||||||
|
line := strings.Split(e,"\t")
|
||||||
|
var title string
|
||||||
|
if len(line[0]) > 1 {
|
||||||
|
title = line[0][1:]
|
||||||
|
} else {
|
||||||
|
title = ""
|
||||||
|
}
|
||||||
|
if len(line[0]) > 0 && string(line[0][0]) == "i" {
|
||||||
|
v.Content[i] = " " + string(title)
|
||||||
|
continue
|
||||||
|
} else if len(line) >= 4 {
|
||||||
|
fulllink := fmt.Sprintf("%s:%s/%s%s", line[2], line[3], string(line[0][0]), line[1])
|
||||||
|
v.Links = append(v.Links, fulllink)
|
||||||
|
linktext := fmt.Sprintf("(%s) %2d %s", GetType(string(line[0][0])), len(v.Links), title)
|
||||||
|
v.Content[i] = linktext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "Display" receiver is called on a view struct.
|
||||||
|
// It prints the content, line by line, of the View.
|
||||||
|
// This receiver does not return anything.
|
||||||
|
func (v View) Display() {
|
||||||
|
fmt.Println()
|
||||||
|
for _, el := range v.Content {
|
||||||
|
fmt.Println(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------\\
|
||||||
|
// + + + F U N C T I O N S + + + \\
|
||||||
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
|
|
||||||
|
// Constructor function for View struct.
|
||||||
|
// This is used to initialize a View with
|
||||||
|
// a Url struct, links, and content. It takes
|
||||||
|
// a Url struct and a content []string and returns
|
||||||
|
// a View (NOT a pointer to a View).
|
||||||
|
func MakeView(url Url, content []string) View {
|
||||||
|
v := View{content, make([]string, 0), url}
|
||||||
|
v.ParseMap()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
|
44
notes.md
44
notes.md
|
@ -1,3 +1,9 @@
|
||||||
|
***TODO
|
||||||
|
- Load homepage on open, if one is set
|
||||||
|
- Add built in help system: SIMPLE :help, DO :help action
|
||||||
|
- Add styles/color support
|
||||||
|
- Revisit the name?
|
||||||
|
|
||||||
Control keys/input:
|
Control keys/input:
|
||||||
|
|
||||||
q quit
|
q quit
|
||||||
|
@ -6,28 +12,31 @@ k scrollup
|
||||||
f toggle showing favorites as subwindow
|
f toggle showing favorites as subwindow
|
||||||
r refresh current page data (re-request)
|
r refresh current page data (re-request)
|
||||||
|
|
||||||
|
GO
|
||||||
:# go to link num
|
:# go to link num
|
||||||
:url go to url
|
:url go to url
|
||||||
|
|
||||||
:w # name write linknum to file
|
SIMPLE
|
||||||
:w url name write url to file
|
:quit quit
|
||||||
:w name write current to file
|
:home visit home
|
||||||
|
:bookmarks toogle bookmarks window
|
||||||
|
|
||||||
:q quit
|
DOLINK
|
||||||
|
:delete # delete bookmark with num
|
||||||
|
:bookmarks # visit bookmark with num
|
||||||
|
|
||||||
:f add #__ name add link num as favorite
|
DOLINKAS
|
||||||
:f add url name add link url as favorite
|
:write # name write linknum to file
|
||||||
:f add name add current page as favorite
|
:add # name add link num as favorite
|
||||||
:f del # delete favorite with num
|
|
||||||
:f del url delete favorite with url
|
|
||||||
:f del name delete favorite with name
|
|
||||||
:f # visit favorite with num
|
|
||||||
|
|
||||||
:s ...kywds search assigned engine with keywords
|
DOAS
|
||||||
|
:write url name write url to file
|
||||||
|
:add url name add link url as favorite
|
||||||
|
:set something something set a system variable
|
||||||
|
|
||||||
:home # set homepage to link num
|
|
||||||
:home url set homepage to url
|
|
||||||
:home visit home
|
value, action, word
|
||||||
|
|
||||||
- - - - - - - - - - - - - - - - - -
|
- - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
@ -38,8 +47,11 @@ colorfield.space ++ gopher://colorfield.space:70/
|
||||||
My phlog ++ gopher://circumlunar.space/1/~sloum/
|
My phlog ++ gopher://circumlunar.space/1/~sloum/
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
homepage ++ gopher://sdf.org
|
home ++ gopher://sdf.org
|
||||||
searchengine ++ gopher://floodgap.place/v2/veronicasomething
|
searchengine ++ gopher://floodgap.place/v2/veronicasomething
|
||||||
savelocation ++ ~/Downloads/
|
savelocation ++ ~/Downloads/
|
||||||
httpbrowser ++ lynx
|
httpbrowser ++ lynx
|
||||||
openhttp ++ true
|
openhttp ++ true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue