From 80ab6524482fa7d4287d4ca87013b3fef18e064d Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Sun, 17 Mar 2019 09:58:39 -0700 Subject: [PATCH] Apparently added a ridiculous mess since last commit --- cmdparse/lexer.go | 170 ++++++++++++++++++++++ cmdparse/parser.go | 142 +++++++++++++++++++ config/lexer.go | 206 +++++++++++++++++++++++++++ config/parser.go | 113 +++++++++++++++ cui/cui.go | 176 ----------------------- cui/msgbar.go | 31 +++++ cui/screen.go | 153 ++++++++++++++++++++ cui/window.go | 104 ++++++++++++++ gclient.go | 341 +++++++++++++++++++++++++++++++++++++++------ gopher/bookmark.go | 57 ++++++++ gopher/gopher.go | 241 -------------------------------- gopher/history.go | 113 +++++++++++++++ gopher/url.go | 89 ++++++++++++ gopher/view.go | 89 ++++++++++++ notes.md | 44 +++--- 15 files changed, 1590 insertions(+), 479 deletions(-) create mode 100644 cmdparse/lexer.go create mode 100644 cmdparse/parser.go create mode 100644 config/lexer.go create mode 100644 config/parser.go create mode 100644 cui/msgbar.go create mode 100644 cui/screen.go create mode 100644 cui/window.go create mode 100644 gopher/bookmark.go create mode 100644 gopher/history.go create mode 100644 gopher/url.go create mode 100644 gopher/view.go diff --git a/cmdparse/lexer.go b/cmdparse/lexer.go new file mode 100644 index 0000000..c61ca74 --- /dev/null +++ b/cmdparse/lexer.go @@ -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) +} + diff --git a/cmdparse/parser.go b/cmdparse/parser.go new file mode 100644 index 0000000..202bfb3 --- /dev/null +++ b/cmdparse/parser.go @@ -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)} +} diff --git a/config/lexer.go b/config/lexer.go new file mode 100644 index 0000000..3be62a2 --- /dev/null +++ b/config/lexer.go @@ -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 +} diff --git a/config/parser.go b/config/parser.go new file mode 100644 index 0000000..8e74ee6 --- /dev/null +++ b/config/parser.go @@ -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)} +} diff --git a/cui/cui.go b/cui/cui.go index 6aedad4..7214ecb 100644 --- a/cui/cui.go +++ b/cui/cui.go @@ -20,165 +20,6 @@ var shapes = map[string]string{ "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) { if val, ok := shapes[shape]; ok { fmt.Printf("%s", val) @@ -267,23 +108,6 @@ func WrapLines(s []string, length int) []string { 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 { reader := bufio.NewReader(os.Stdin) char, _, err := reader.ReadRune() diff --git a/cui/msgbar.go b/cui/msgbar.go new file mode 100644 index 0000000..21dcb1e --- /dev/null +++ b/cui/msgbar.go @@ -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") +} diff --git a/cui/screen.go b/cui/screen.go new file mode 100644 index 0000000..ea47b99 --- /dev/null +++ b/cui/screen.go @@ -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 +} diff --git a/cui/window.go b/cui/window.go new file mode 100644 index 0000000..0591a69 --- /dev/null +++ b/cui/window.go @@ -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") + } +} + diff --git a/gclient.go b/gclient.go index 4388696..9dd4e95 100644 --- a/gclient.go +++ b/gclient.go @@ -1,28 +1,56 @@ package main import ( - "fmt" "gsock/gopher" - "os" + "gsock/cmdparse" + "gsock/config" + "gsock/cui" "os/user" + "io/ioutil" + "os" + "fmt" + "strings" "regexp" "strconv" - "gsock/cui" - "errors" ) var history gopher.History = gopher.MakeHistory() var screen *cui.Screen 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) { fmt.Println(err) os.Exit(code) } -func save_file() { - //TODO add a way to save a file... - //eg. :save 5 test.txt +func save_file(address, name string) error { + quickMessage("Saving file...", false) + 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 { @@ -42,16 +70,93 @@ func search(u string) error { } -func route_input(s string) error { - if num, _ := regexp.MatchString(`^\d+$`, s); num && history.Length > 0 { - linkcount := len(history.Collection[history.Position].Links) - item, _ := strconv.Atoi(s) - if item <= linkcount { - linkurl := history.Collection[history.Position].Links[item - 1] - v, err := gopher.Visit(linkurl) +func route_input(com *cmdparse.Command) error { + var err error + switch com.Type { + case cmdparse.SIMPLE: + err = simple_command(com.Action) + case cmdparse.GOURL: + 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 { 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" { err := search(linkurl) @@ -59,61 +164,200 @@ func route_input(s string) error { return err } } else if v.Address.IsBinary { - // TODO add download querying here + // TO DO: run this into the write to file method } else { history.Add(v) } } else { - errname := fmt.Sprintf("Invalid link id: %s", s) - return errors.New(errname) + return fmt.Errorf("Invalid link id: %s", l) } } else { - v, err := gopher.Visit(s) - 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 fmt.Errorf("Invalid link id: %s", l) } 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() { - // fmt.Println(userinfo.HomeDir) +func do_link_command(action, target string) error { + 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 screen = cui.NewScreen() 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() - screen.AddWindow(1, 1, screen.Height - 2, screen.Width, false, false) + initClient() mainWindow := screen.Windows[0] + first_load := true + redrawScreen := true for { 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 c := cui.Getch() switch c { case 'j', 'J': - mainWindow.ScrollDown() + screen.Windows[screen.Activewindow].ScrollDown() case 'k', 'K': - mainWindow.ScrollUp() + screen.Windows[screen.Activewindow].ScrollUp() case 'q', 'Q': cui.Exit() - case 'b', 'B': + case 'b': history.GoBack() mainWindow.Scrollposition = 0 redrawScreen = true + case 'B': + toggle_bookmarks() case 'f', 'F': history.GoForward() mainWindow.Scrollposition = 0 @@ -122,28 +366,33 @@ func main() { redrawScreen = true cui.MoveCursorTo(screen.Height - 1, 0) entry := cui.GetLine() - // Clear entry line - cui.MoveCursorTo(screen.Height - 1, 0) - cui.Clear("line") + // Clear entry line and error line + clearInput(true) if entry == "" { - cui.MoveCursorTo(screen.Height - 1, 0) - fmt.Print(" ") + redrawScreen = false continue } - err := route_input(entry) + parser := cmdparse.NewParser(strings.NewReader(entry)) + p, err := parser.Parse() if err != nil { - // Display error cui.MoveCursorTo(screen.Height, 0) fmt.Print("\033[41m\033[37m", err, "\033[0m") // Set screen to not reflash redrawScreen = false } else { - mainWindow.Scrollposition = 0 - // screen.Clear() + err := route_input(p) + 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 { mainWindow.Content = history.Collection[history.Position].Content + screen.Bars[0].SetMessage(history.Collection[history.Position].Address.Full) } } } diff --git a/gopher/bookmark.go b/gopher/bookmark.go new file mode 100644 index 0000000..f8e1273 --- /dev/null +++ b/gopher/bookmark.go @@ -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{}} +} diff --git a/gopher/gopher.go b/gopher/gopher.go index c284158..8a0543b 100644 --- a/gopher/gopher.go +++ b/gopher/gopher.go @@ -4,56 +4,14 @@ package gopher import ( - "fmt" "strings" "errors" - "regexp" "net" "io/ioutil" "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 + + + \\ //--------------------------------------------------\\ @@ -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 + + + \\ //--------------------------------------------------\\ -// 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(`^((?Pgopher|http|https|ftp|telnet):\/\/)?(?P[\w\-\.\d]+)(?::(?P\d+)?)?(?:/(?P[01345679gIhisp])?)?(?P(?:\/.*)?)?$`) - 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 // the response as []byte/error. This function is diff --git a/gopher/history.go b/gopher/history.go new file mode 100644 index 0000000..2c9e30f --- /dev/null +++ b/gopher/history.go @@ -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{}} +} diff --git a/gopher/url.go b/gopher/url.go new file mode 100644 index 0000000..f029e0b --- /dev/null +++ b/gopher/url.go @@ -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(`^((?Pgopher|http|https|ftp|telnet):\/\/)?(?P[\w\-\.\d]+)(?::(?P\d+)?)?(?:/(?P[01345679gIhisp])?)?(?P(?:\/.*)?)?$`) + 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 +} + diff --git a/gopher/view.go b/gopher/view.go new file mode 100644 index 0000000..21f27c5 --- /dev/null +++ b/gopher/view.go @@ -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 +} + + diff --git a/notes.md b/notes.md index 627733b..7526693 100644 --- a/notes.md +++ b/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: q quit @@ -6,28 +12,31 @@ k scrollup f toggle showing favorites as subwindow r refresh current page data (re-request) +GO :# go to link num :url go to url -:w # name write linknum to file -:w url name write url to file -:w name write current to file +SIMPLE +:quit quit +: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 -:f add url name add link url as favorite -:f add name add current page 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 +DOLINKAS +:write # name write linknum to file +:add # name add link num as favorite -: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/ [options] -homepage ++ gopher://sdf.org +home ++ gopher://sdf.org searchengine ++ gopher://floodgap.place/v2/veronicasomething savelocation ++ ~/Downloads/ httpbrowser ++ lynx openhttp ++ true + + +