Functional browser added notes

This commit is contained in:
sloumdrone 2019-03-04 23:05:43 -08:00
parent 0398caf9e3
commit a28fc00550
5 changed files with 509 additions and 131 deletions

314
cui/cui.go Normal file
View File

@ -0,0 +1,314 @@
package cui
import (
"strings"
"bytes"
"fmt"
"bufio"
"os"
"os/exec"
)
var shapes = map[string]string{
"wall": "│",
"ceiling": "─",
"tl": "┌",
"tr": "┐",
"bl": "└",
"br": "┘",
"scroll-thumb": "▉",
"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)
} else {
fmt.Print("x")
}
}
func moveThenDrawShape(r, c int, s string) {
MoveCursorTo(r, c)
drawShape(s)
}
func MoveCursorTo(row, col int) {
fmt.Printf("\033[%d;%dH", row, col)
}
func moveCursorToward(dir string, amount int) {
directions := map[string]string{
"up": "A",
"down": "B",
"left": "D",
"right": "C",
}
if val, ok := directions[dir]; ok {
fmt.Printf("\033[%d%s", amount, val)
}
}
func Exit() {
moveCursorToward("down", 500)
moveCursorToward("right", 500)
SetLineMode()
fmt.Print("\n")
fmt.Print("\033[?25h")
os.Exit(0)
}
func Clear(dir string) {
directions := map[string]string{
"up": "\033[1J",
"down": "\033[0J",
"left": "\033[1K",
"right": "\033[0K",
"line": "\033[2K",
"screen": "\033[2J",
}
if val, ok := directions[dir]; ok {
fmt.Print(val)
}
}
func WrapLines(s []string, length int) []string {
out := []string{}
for _, ln := range s {
if len(ln) <= length {
out = append(out, ln)
} else {
words := strings.Split(ln, " ")
var subout bytes.Buffer
for i, wd := range words {
sublen := subout.Len()
if sublen + len(wd) + 1 <= length {
if sublen > 0 {
subout.WriteString(" ")
}
subout.WriteString(wd)
if i == len(words) - 1 {
out = append(out, subout.String())
}
} else {
out = append(out, subout.String())
subout.Reset()
subout.WriteString(wd)
if i == len(words) - 1 {
out = append(out, subout.String())
subout.Reset()
}
}
}
}
}
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()
if err != nil {
return '@'
}
return char
}
func GetLine() string {
SetLineMode()
reader := bufio.NewReader(os.Stdin)
fmt.Print(": ")
text, _ := reader.ReadString('\n')
SetCharMode()
return text[:len(text)-1]
}
func SetCharMode() {
exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
fmt.Print("\033[?25l")
}
func SetLineMode() {
exec.Command("stty", "-F", "/dev/tty", "-cbreak").Run()
exec.Command("stty", "-F", "/dev/tty", "echo").Run()
}

View File

@ -4,92 +4,146 @@ import (
"fmt"
"gsock/gopher"
"os"
"bufio"
"os/user"
"regexp"
"strings"
"strconv"
"gsock/cui"
"errors"
)
var history gopher.History = gopher.MakeHistory()
var screen *cui.Screen
var userinfo, _ = user.Current()
func err_exit(err string, code int) {
fmt.Println(err)
os.Exit(code)
}
func getln() string {
reader := bufio.NewReader(os.Stdin)
fmt.Print(": ")
text, _ := reader.ReadString('\n')
return text[:len(text)-1]
func save_file() {
//TODO add a way to save a file...
//eg. :save 5 test.txt
}
func search(u string) error {
cui.MoveCursorTo(screen.Height - 1, 0)
cui.Clear("line")
fmt.Print("Enter form input: ")
cui.MoveCursorTo(screen.Height - 1, 17)
entry := cui.GetLine()
searchurl := fmt.Sprintf("%s\t%s", u, entry[:len(entry) - 1])
sv, err := gopher.Visit(searchurl)
if err != nil {
return err
}
history.Add(sv)
return nil
}
func route_input(s string) {
sl := strings.ToLower(s)
if sl == "quit" || sl == "exit" || sl == "q" {
err_exit("Quitting...", 0)
} else if num, _ := regexp.MatchString(`^\d+$`, s); num && history.Length > 0 {
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 := history.Visit(linkurl)
v, err := gopher.Visit(linkurl)
if err != nil {
fmt.Println(err.Error())
return err
}
if v.Address.IsBinary {
// Query for download here
fmt.Println("Would you like to download this file?")
if v.Address.Gophertype == "7" {
err := search(linkurl)
if err != nil {
return err
}
} else if v.Address.IsBinary {
// TODO add download querying here
} else {
history.Add(v)
history.DisplayCurrentView()
}
} else {
fmt.Println("Invalid link id")
errname := fmt.Sprintf("Invalid link id: %s", s)
return errors.New(errname)
}
} else if sl == "back" || sl == "b" {
history.GoBack()
} else if sl == "forward" || sl == "f" {
history.GoForward()
} else {
v, err := history.Visit(s)
v, err := gopher.Visit(s)
if err != nil {
fmt.Println(err.Error())
return err
}
if v.Address.IsBinary {
// Query for download here
fmt.Println("Would you like to download this file?")
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)
history.DisplayCurrentView()
}
}
return nil
}
func make_request(s string) ([]string, gopher.Url, error) {
u, _ := gopher.MakeUrl(s)
text, err := gopher.Retrieve(u)
if err != nil {
return []string{}, u, err
}
return strings.Split(string(text), "\n"), u, nil
}
func main() {
// fmt.Println(userinfo.HomeDir)
history.Position = -1
var inp string
if len(os.Args) >= 2 {
inp = os.Args[1]
route_input(inp)
}
screen = cui.NewScreen()
screen.SetCharMode()
defer cui.Exit()
screen.AddWindow(1, 1, screen.Height - 2, screen.Width, false, false)
mainWindow := screen.Windows[0]
redrawScreen := true
for {
inp = getln()
if inp == "" {
continue
screen.ReflashScreen(redrawScreen)
redrawScreen = false
c := cui.Getch()
switch c {
case 'j', 'J':
mainWindow.ScrollDown()
case 'k', 'K':
mainWindow.ScrollUp()
case 'q', 'Q':
cui.Exit()
case 'b', 'B':
history.GoBack()
mainWindow.Scrollposition = 0
redrawScreen = true
case 'f', 'F':
history.GoForward()
mainWindow.Scrollposition = 0
redrawScreen = true
case ':':
redrawScreen = true
cui.MoveCursorTo(screen.Height - 1, 0)
entry := cui.GetLine()
// Clear entry line
cui.MoveCursorTo(screen.Height - 1, 0)
cui.Clear("line")
if entry == "" {
cui.MoveCursorTo(screen.Height - 1, 0)
fmt.Print(" ")
continue
}
err := route_input(entry)
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()
}
}
if history.Position >= 0 {
mainWindow.Content = history.Collection[history.Position].Content
}
route_input(inp)
}
}

View File

@ -60,7 +60,7 @@ type Url struct {
// Types is a map of gophertypes to a string representing their
// type, to be used when displaying gophermaps
var Types = map[string]string{
var types = map[string]string{
"0": "TXT",
"1": "MAP",
"h": "HTM",
@ -123,7 +123,8 @@ func (h History) Get() (*View, error) {
func (h *History) GoBack() {
if h.Position > 0 {
h.Position--
h.DisplayCurrentView()
} else {
fmt.Print("\a")
}
}
@ -136,6 +137,8 @@ func (h *History) GoForward() {
if h.Position + 1 < h.Length {
h.Position++
h.DisplayCurrentView()
} else {
fmt.Print("\a")
}
}
@ -148,33 +151,6 @@ func (h *History) DisplayCurrentView() {
h.Collection[h.Position].Display()
}
// The "Visit" receiver is a high level combination of a few
// different receivers that makes it easy to create a Url,
// make a request to that Url, and add the response and Url
// to a View. That View then gets added to the History struct
// that the Visit receiver was called on. Returns a boolean
// value indicating whether or not the content is binary or
// textual data.
func (h *History) Visit(addr string) (View, error) {
u, err := MakeUrl(addr)
if err != nil {
return View{}, err
}
text, err := Retrieve(u)
if err != nil {
return View{}, err
}
var pageContent []string
if u.IsBinary {
pageContent = []string{string(text)}
} else {
pageContent = strings.Split(string(text), "\n")
}
return MakeView(u, pageContent), nil
}
// The "ParseMap" receiver is called by a view struct. It
// checks if the view is for a gophermap. If not,it does
@ -185,19 +161,25 @@ func (h *History) Visit(addr string) (View, error) {
// relates to the link position in the links slice. This
// receiver does not return anything.
func (v *View) ParseMap() {
if v.Address.Gophertype == "1" {
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(line[0][1:])
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", Types[string(line[0][0])], len(v.Links), string(line[0][1:]))
linktext := fmt.Sprintf("(%s) %2d %s", GetType(string(line[0][0])), len(v.Links), title)
v.Content[i] = linktext
}
}
}
}
}
@ -208,9 +190,7 @@ func (v *View) ParseMap() {
func (v View) Display() {
fmt.Println()
for _, el := range v.Content {
if el != "." {
fmt.Println(el)
}
fmt.Println(el)
}
}
@ -341,3 +321,36 @@ func Retrieve(u Url) ([]byte, error) {
}
// The "Visit" function is a high level combination of a few
// different types that makes it easy to create a Url,
// make a request to that Url, and add the response and Url
// to a View. Returns a copy of the view and an error (or nil).
func Visit(addr string) (View, error) {
u, err := MakeUrl(addr)
if err != nil {
return View{}, err
}
text, err := Retrieve(u)
if err != nil {
return View{}, err
}
var pageContent []string
if u.IsBinary && u.Gophertype != "7" {
pageContent = []string{string(text)}
} else {
pageContent = strings.Split(string(text), "\n")
}
return MakeView(u, pageContent), nil
}
func GetType(t string) string {
if val, ok := types[t]; ok {
return val
}
return "???"
}

45
notes.md Normal file
View File

@ -0,0 +1,45 @@
Control keys/input:
q quit
j scrolldown
k scrollup
f toggle showing favorites as subwindow
r refresh current page data (re-request)
:# 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
:q quit
: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
:s ...kywds search assigned engine with keywords
:home # set homepage to link num
:home url set homepage to url
:home visit home
- - - - - - - - - - - - - - - - - -
Config format:
[favorites]
colorfield.space ++ gopher://colorfield.space:70/
My phlog ++ gopher://circumlunar.space/1/~sloum/
[options]
homepage ++ gopher://sdf.org
searchengine ++ gopher://floodgap.place/v2/veronicasomething
savelocation ++ ~/Downloads/
httpbrowser ++ lynx
openhttp ++ true

View File

@ -1,48 +0,0 @@
package socket
import (
"net"
"io/ioutil"
"gsock/gopher"
"errors"
)
func Retrieve(u gopher.Url) ([]byte, error) {
nullRes := make([]byte, 0)
if u.Host == "" || u.Port == "" {
return nullRes, errors.New("Incomplete request url")
}
addr := u.Host + ":" + u.Port
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
errortext := "Could not find host: " + u.Full
return nullRes, errors.New(errortext)
}
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
return nullRes, err
}
send := u.Resource + "\n"
if u.Scheme == "http" || u.Scheme == "https" {
send = u.Gophertype
}
_, err = conn.Write([]byte(send))
if err != nil {
return nullRes, err
}
result, err := ioutil.ReadAll(conn)
if err != nil {
return nullRes, err
}
return result, err
}