bombadillo/0000755000175000017500000000000014211226262012506 5ustar nileshnileshbombadillo/url.go0000644000175000017500000001110614211226262013636 0ustar nileshnileshpackage main import ( "fmt" "os/user" "path" "path/filepath" "regexp" "strings" ) //------------------------------------------------\\ // + + + T Y P E S + + + \\ //--------------------------------------------------\\ // Url is a struct representing the different pieces // of a url. This custom struct is used rather than the // built-in url library so-as to support gopher URLs, as // well as track mime-type and renderability (can the // response to the url be rendered as text in the client). type Url struct { Scheme string Host string Port string Resource string Full string Mime string DownloadOnly bool } //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ // There are currently no receivers for the Url struct //------------------------------------------------\\ // + + + 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) { if len(u) < 1 { return Url{}, fmt.Errorf("Invalid url, unable to parse") } if strings.HasPrefix(u, "finger://") { return parseFinger(u) } var out Url if local := strings.HasPrefix(u, "local://"); u[0] == '/' || u[0] == '.' || u[0] == '~' || local { if local && len(u) > 8 { u = u[8:] } var home string userinfo, err := user.Current() if err != nil { home = "" } else { home = userinfo.HomeDir } u = strings.Replace(u, "~", home, 1) res, err := filepath.Abs(u) if err != nil { return out, fmt.Errorf("Invalid path, unable to parse") } out.Scheme = "local" out.Host = "" out.Port = "" out.Mime = "" out.Resource = res out.Full = out.Scheme + "://" + out.Resource return out, nil } re := regexp.MustCompile(`^((?P[a-zA-Z]+):\/\/)?(?P[\w\-\.\d]+)(?::(?P\d+)?)?(?:/(?P[01345679gIhisp])?)?(?P.*)?$`) match := re.FindStringSubmatch(u) if valid := re.MatchString(u); !valid { return out, fmt.Errorf("Invalid url, unable to parse") } 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.Mime = match[i] case "resource": out.Resource = match[i] } } if out.Host == "" { return out, fmt.Errorf("no host") } out.Scheme = strings.ToLower(out.Scheme) if out.Scheme == "" { out.Scheme = bombadillo.Options["defaultscheme"] } if out.Scheme == "gopher" && out.Port == "" { out.Port = "70" } else if out.Scheme == "http" && out.Port == "" { out.Port = "80" } else if out.Scheme == "https" && out.Port == "" { out.Port = "443" } else if out.Scheme == "gemini" && out.Port == "" { out.Port = "1965" } else if out.Scheme == "telnet" && out.Port == "" { out.Port = "23" } if out.Scheme == "gopher" { if out.Mime == "" { out.Mime = "1" } if out.Resource == "" || out.Resource == "/" { out.Mime = "1" } if out.Mime == "7" && strings.Contains(out.Resource, "\t") { out.Mime = "1" } switch out.Mime { case "1", "0", "h", "7", "I", "g": out.DownloadOnly = false default: out.DownloadOnly = true } } else { out.Resource = fmt.Sprintf("%s%s", out.Mime, out.Resource) out.Mime = "" } out.Full = out.Scheme + "://" + out.Host + ":" + out.Port + "/" + out.Mime + out.Resource return out, nil } func UpOneDir(u string) string { url, err := MakeUrl(u) if len(url.Resource) < 1 || err != nil { return u } if strings.HasSuffix(url.Resource, "/") { url.Resource = url.Resource[:len(url.Resource)-1] } url.Resource, _ = path.Split(url.Resource) if url.Scheme == "gopher" { url.Mime = "1" } url.Full = url.Scheme + "://" + url.Host + ":" + url.Port + "/" + url.Mime + url.Resource return url.Full } func parseFinger(u string) (Url, error) { var out Url out.Scheme = "finger" if len(u) < 10 { return out, fmt.Errorf("Invalid finger address") } u = u[9:] userPlusAddress := strings.Split(u, "@") if len(userPlusAddress) > 1 { out.Resource = userPlusAddress[0] u = userPlusAddress[1] } hostPort := strings.Split(u, ":") if len(hostPort) < 2 { out.Port = "79" } else { out.Port = hostPort[1] } out.Host = hostPort[0] resource := "" if out.Resource != "" { resource = out.Resource + "@" } out.Full = fmt.Sprintf("%s://%s%s:%s", out.Scheme, resource, out.Host, out.Port) return out, nil } bombadillo/termios/0000755000175000017500000000000014211226262014170 5ustar nileshnileshbombadillo/termios/termios.go0000644000175000017500000000210014211226262016172 0ustar nileshnileshpackage termios import ( "os" "runtime" "syscall" "unsafe" ) type winsize struct { Row uint16 Col uint16 Xpixel uint16 Ypixel uint16 } var fd = os.Stdin.Fd() func ioctl(fd, request, argp uintptr) error { if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, request, argp); e != 0 { return e } return nil } func GetWindowSize() (int, int) { var value winsize ioctl(fd, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&value))) return int(value.Col), int(value.Row) } func getTermios() syscall.Termios { var value syscall.Termios err := ioctl(fd, getTermiosIoctl, uintptr(unsafe.Pointer(&value))) if err != nil { panic(err) } return value } func setTermios(termios syscall.Termios) { err := ioctl(fd, setTermiosIoctl, uintptr(unsafe.Pointer(&termios))) if err != nil { panic(err) } runtime.KeepAlive(termios) } func SetCharMode() { t := getTermios() t.Lflag = t.Lflag ^ syscall.ICANON t.Lflag = t.Lflag ^ syscall.ECHO setTermios(t) } func SetLineMode() { var t = getTermios() t.Lflag = t.Lflag | (syscall.ICANON | syscall.ECHO) setTermios(t) } bombadillo/termios/consts_nonlinux.go0000644000175000017500000000021014211226262017753 0ustar nileshnilesh// +build !linux package termios import "syscall" const ( getTermiosIoctl = syscall.TIOCGETA setTermiosIoctl = syscall.TIOCSETAF ) bombadillo/termios/consts_linux.go0000644000175000017500000000020214211226262017241 0ustar nileshnilesh// +build linux package termios import "syscall" const ( getTermiosIoctl = syscall.TCGETS setTermiosIoctl = syscall.TCSETS ) bombadillo/telnet/0000755000175000017500000000000014211226262014001 5ustar nileshnileshbombadillo/telnet/telnet.go0000644000175000017500000000173114211226262015625 0ustar nileshnilesh// Package telnet provides a function that starts a telnet session in a subprocess. package telnet import ( "fmt" "os" "os/exec" "tildegit.org/sloum/bombadillo/cui" ) // StartSession starts a telnet session as a subprocess, connecting to the host // and port specified. Telnet is run interactively as a subprocess until the // process ends. It returns any errors from the telnet session. func StartSession(host string, port string) (string, error) { c := exec.Command("telnet", host, port) c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr // Clear the screen and position the cursor at the top left fmt.Print("\033[2J\033[0;0H") // Defer reset and reinit of the terminal to prevent any changes from // telnet carrying over to the client (or beyond...) defer func() { cui.Tput("reset") cui.InitTerm() }() err := c.Run() if err != nil { return "", fmt.Errorf("Telnet error response: %s", err.Error()) } return "Telnet session terminated", nil } bombadillo/tdiv/0000755000175000017500000000000014211226262013454 5ustar nileshnileshbombadillo/tdiv/tdiv.go0000644000175000017500000001342514211226262014756 0ustar nileshnileshpackage tdiv import ( "bytes" "fmt" "image" "image/gif" "image/jpeg" "image/png" "io" "strings" ) func getBraille(pattern string) (rune, error) { switch pattern { case "000000": return ' ', nil case "100000": return '⠁', nil case "001000": return '⠂', nil case "101000": return '⠃', nil case "000010": return '⠄', nil case "100010": return '⠅', nil case "001010": return '⠆', nil case "101010": return '⠇', nil case "010000": return '⠈', nil case "110000": return '⠉', nil case "011000": return '⠊', nil case "111000": return '⠋', nil case "010010": return '⠌', nil case "110010": return '⠍', nil case "011010": return '⠎', nil case "111010": return '⠏', nil case "000100": return '⠐', nil case "100100": return '⠑', nil case "001100": return '⠒', nil case "101100": return '⠓', nil case "000110": return '⠔', nil case "100110": return '⠕', nil case "001110": return '⠖', nil case "101110": return '⠗', nil case "010100": return '⠘', nil case "110100": return '⠙', nil case "011100": return '⠚', nil case "111100": return '⠛', nil case "010110": return '⠜', nil case "110110": return '⠝', nil case "011110": return '⠞', nil case "111110": return '⠟', nil case "000001": return '⠠', nil case "100001": return '⠡', nil case "001001": return '⠢', nil case "101001": return '⠣', nil case "000011": return '⠤', nil case "100011": return '⠥', nil case "001011": return '⠦', nil case "101011": return '⠧', nil case "010001": return '⠨', nil case "110001": return '⠩', nil case "011001": return '⠪', nil case "111001": return '⠫', nil case "010011": return '⠬', nil case "110011": return '⠭', nil case "011011": return '⠮', nil case "111011": return '⠯', nil case "000101": return '⠰', nil case "100101": return '⠱', nil case "001101": return '⠲', nil case "101101": return '⠳', nil case "000111": return '⠴', nil case "100111": return '⠵', nil case "001111": return '⠶', nil case "101111": return '⠷', nil case "010101": return '⠸', nil case "110101": return '⠹', nil case "011101": return '⠺', nil case "111101": return '⠻', nil case "010111": return '⠼', nil case "110111": return '⠽', nil case "011111": return '⠾', nil case "111111": return '⠿', nil default: return '!', fmt.Errorf("Invalid character entry") } } // scaleImage loads and scales an image and returns a 2d pixel-int slice // // Adapted from: // http://tech-algorithm.com/articles/nearest-neighbor-image-scaling/ func scaleImage(file io.Reader, newWidth int) (int, int, [][]int, error) { img, _, err := image.Decode(file) if err != nil { return 0, 0, nil, err } bounds := img.Bounds() width, height := bounds.Max.X, bounds.Max.Y newHeight := int(float64(newWidth) * (float64(height) / float64(width))) out := make([][]int, newHeight) for i := range out { out[i] = make([]int, newWidth) } xRatio := float64(width) / float64(newWidth) yRatio := float64(height) / float64(newHeight) var px, py int for i := 0; i < newHeight; i++ { for j := 0; j < newWidth; j++ { px = int(float64(j) * xRatio) py = int(float64(i) * yRatio) out[i][j] = rgbaToGray(img.At(px, py).RGBA()) } } return newWidth, newHeight, out, nil } // Get the bi-dimensional pixel array func getPixels(file io.Reader) (int, int, [][]int, error) { img, _, err := image.Decode(file) if err != nil { return 0, 0, nil, err } bounds := img.Bounds() width, height := bounds.Max.X, bounds.Max.Y var pixels [][]int for y := 0; y < height; y++ { var row []int for x := 0; x < width; x++ { row = append(row, rgbaToGray(img.At(x, y).RGBA())) } pixels = append(pixels, row) } return width, height, pixels, nil } func errorDither(w, h int, p [][]int) [][]int { mv := [4][2]int{ [2]int{0, 1}, [2]int{1, 1}, [2]int{1, 0}, [2]int{1, -1}, } per := [4]float64{0.4375, 0.0625, 0.3125, 0.1875} var res, diff int for y := 0; y < h; y++ { for x := 0; x < w; x++ { cur := p[y][x] if cur > 128 { res = 1 diff = -(255 - cur) } else { res = 0 diff = cur // TODO see why this was abs() in the py version } for i, v := range mv { if y+v[0] >= h || x+v[1] >= w || x+v[1] <= 0 { continue } px := p[y+v[0]][x+v[1]] px = int(float64(diff)*per[i] + float64(px)) if px < 0 { px = 0 } else if px > 255 { px = 255 } p[y+v[0]][x+v[1]] = px p[y][x] = res } } } return p } func toBraille(p [][]int) []rune { w := len(p[0]) // TODO this is unsafe h := len(p) rows := h / 3 cols := w / 2 out := make([]rune, rows*(cols+1)) counter := 0 for y := 0; y < h-3; y += 4 { for x := 0; x < w-1; x += 2 { str := fmt.Sprintf( "%d%d%d%d%d%d", p[y][x], p[y][x+1], p[y+1][x], p[y+1][x+1], p[y+2][x], p[y+2][x+1]) b, err := getBraille(str) if err != nil { out[counter] = ' ' } else { out[counter] = b } counter++ } out[counter] = '\n' counter++ } return out } func rgbaToGray(r uint32, g uint32, b uint32, a uint32) int { rf := float64(r/257) * 0.92126 gf := float64(g/257) * 0.97152 bf := float64(b/257) * 0.90722 grey := int((rf + gf + bf) / 3) return grey } func Render(in []byte, width int) []string { image.RegisterFormat("jpeg", "jpeg", jpeg.Decode, jpeg.DecodeConfig) image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig) image.RegisterFormat("gif", "gif", gif.Decode, gif.DecodeConfig) w, h, p, err := scaleImage(bytes.NewReader(in), width) if err != nil { return []string{"Unable to render image.", "Please download using:", "", " :w ."} } px := errorDither(w, h, p) b := toBraille(px) out := strings.SplitN(string(b), "\n", -1) return out } bombadillo/pages.go0000644000175000017500000000612214211226262014135 0ustar nileshnileshpackage main import ( "fmt" ) //------------------------------------------------\\ // + + + T Y P E S + + + \\ //--------------------------------------------------\\ // Pages is a struct that represents the history of the client. // It functions as a container for the pages (history array) and // tracks the current history length and location. type Pages struct { Position int Length int History [20]Page } //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ // NavigateHistory takes a positive or negative integer // and updates the current history position. Checks are done // to make sure that the position moved to is a valid history // location. Returns an error or nil. func (p *Pages) NavigateHistory(qty int) error { newPosition := p.Position + qty if newPosition < 0 { return fmt.Errorf("You are already at the beginning of history") } else if newPosition > p.Length-1 { return fmt.Errorf("Your way is blocked by void, there is nothing forward") } p.Position = newPosition return nil } // Add gets passed a Page, which gets added to the history // array. Add also updates the current length and position // of the Pages struct to which it belongs. Add also shifts // off array items if necessary. func (p *Pages) Add(pg Page) { if p.Position == p.Length-1 && p.Length < len(p.History) { p.History[p.Length] = pg p.Length++ p.Position++ } else if p.Position == p.Length-1 && p.Length == 20 { for x := 1; x < len(p.History); x++ { p.History[x-1] = p.History[x] } p.History[len(p.History)-1] = pg } else { p.Position++ p.Length = p.Position + 1 p.History[p.Position] = pg } } // Render wraps the content for the current page and returns // the page content as a string slice func (p *Pages) Render(termHeight, termWidth, maxWidth int, color bool) []string { if p.Length < 1 { return make([]string, 0) } pos := p.History[p.Position].ScrollPosition prev := len(p.History[p.Position].WrappedContent) if termWidth != p.History[p.Position].WrapWidth || p.History[p.Position].Color != color { p.History[p.Position].WrapContent(termWidth, maxWidth, color) } now := len(p.History[p.Position].WrappedContent) if prev > now { diff := prev - now pos = pos - diff } else if prev < now { diff := now - prev pos = pos + diff if pos > now-termHeight { pos = now - termHeight } } if pos < 0 || now < termHeight-3 { pos = 0 } p.History[p.Position].ScrollPosition = pos return p.History[p.Position].WrappedContent[pos:] } func (p *Pages) CopyHistory(pos int) error { if p.Length < 2 || pos > p.Position { return fmt.Errorf("There are not enough history locations available") } if pos < 0 { pos = p.Position-1 } p.Add(p.History[pos]) return nil } //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ // MakePages returns a Pages struct with default values func MakePages() Pages { return Pages{-1, 0, [20]Page{}} } bombadillo/page_test.go0000644000175000017500000000422414211226262015012 0ustar nileshnileshpackage main import ( "reflect" "testing" ) func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { type fields struct { WrappedContent []string RawContent string Links []string Location Url ScrollPosition int FoundLinkLines []int SearchTerm string SearchIndex int FileType string WrapWidth int Color bool } type args struct { width int maxWidth int color bool } // create a Url for use by the MakePage function url, _ := MakeUrl("gemini://rawtext.club") tests := []struct { name string input string expects []string args args }{ { "Short line that doesn't wrap", "0123456789\n", []string{ "0123456789", "", }, args{ 10, 10, false, }, }, { "multiple words should wrap at the right point", "01 345 789 123456789 123456789 123456789 123456789\n", []string{ "01 345 789", "123456789 ", "123456789 ", "123456789 ", "123456789", "", }, args{ 10, 10, false, }, }, { "Long line wrapped to 10 columns, leading spaces omitted when wrapping", "0123456789 123456789 123456789 123456789 123456789\n", []string{ "0123456789", "123456789 ", "123456789 ", "123456789 ", "123456789", "", }, args{ 10, 10, false, }, }, { "Intentional leading spaces aren't trimmed", "01 345\n 789 123456789\n", []string{ "01 345", " 789 ", "123456789", "", }, args{ 10, 10, false, }, }, { "Unicode line endings that should not wrap", "LF\u000A" + "CR+LF\u000D\u000A" + "NEL\u0085" + "LS\u2028" + "PS\u2029", []string{ "LF", "CR+LF", "NEL", "LS", "PS", "", }, args{ 10, 10, false, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := MakePage(url, tt.input, []string{""}) p.WrapContent(tt.args.width-1, tt.args.maxWidth, tt.args.color) if !reflect.DeepEqual(p.WrappedContent, tt.expects) { t.Errorf("Test failed - %s\nexpects %s\nactual %s", tt.name, tt.expects, p.WrappedContent) } }) } } bombadillo/page.go0000644000175000017500000001262214211226262013754 0ustar nileshnileshpackage main import ( "fmt" "strings" "tildegit.org/sloum/bombadillo/tdiv" "unicode" ) //------------------------------------------------\\ // + + + T Y P E S + + + \\ //--------------------------------------------------\\ // Page represents a visited URL's contents; including // the raw content, wrapped content, link slice, URL, // and the current scroll position type Page struct { WrappedContent []string RawContent string Links []string Location Url ScrollPosition int FoundLinkLines []int SearchTerm string SearchIndex int FileType string WrapWidth int Color bool } //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ // ScrollPositionRange may not be in actual usage.... // TODO: find where this is being used func (p *Page) ScrollPositionRange(termHeight int) (int, int) { termHeight -= 3 if len(p.WrappedContent)-p.ScrollPosition < termHeight { p.ScrollPosition = len(p.WrappedContent) - termHeight } if p.ScrollPosition < 0 { p.ScrollPosition = 0 } var end int if len(p.WrappedContent) < termHeight { end = len(p.WrappedContent) } else { end = p.ScrollPosition + termHeight } return p.ScrollPosition, end } func (p *Page) RenderImage(width int) { w := (width - 5) * 2 if w > 300 { w = 300 } p.WrappedContent = tdiv.Render([]byte(p.RawContent), w) p.WrapWidth = width } // WrapContent performs a hard wrap to the requested // width and updates the WrappedContent // of the Page struct width a string slice // of the wrapped data func (p *Page) WrapContent(width, maxWidth int, color bool) { if p.FileType == "image" { p.RenderImage(width) return } width = min(width, maxWidth) counter := 0 spacer := "" var content strings.Builder var esc strings.Builder escape := false content.Grow(len(p.RawContent)) if p.Location.Mime == "1" { // gopher document spacer = " " } else if strings.HasSuffix(p.Location.Mime, "gemini") { //gemini document spacer = " " } runeArr := []rune(p.RawContent) for i := 0; i < len(runeArr); i++ { ch := runeArr[i] if escape { if color { esc.WriteRune(ch) } if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') { escape = false if ch == 'm' { content.WriteString(esc.String()) esc.Reset() } } continue } if ch == '\n' || ch == '\u0085' || ch == '\u2028' || ch == '\u2029' { content.WriteRune('\n') counter = 0 } else if ch == '\t' { if counter+4 < width { content.WriteString(" ") counter += 4 } else { content.WriteRune('\n') counter = 0 } } else if ch == '\r' || ch == '\v' || ch == '\b' || ch == '\f' || ch == '\a' { // Get rid of control characters we don't want continue } else if ch == 27 { if p.Location.Scheme == "local" { if counter+4 >= width { content.WriteRune('\n') } content.WriteString("\\033") continue } escape = true if color { esc.WriteRune(ch) } continue } else { // peek forward to see if we can render the word without going over j := i for ; j < len(runeArr) && !unicode.IsSpace(runeArr[j]); j++ { if counter+(j-i) > width+1 { break } } // if we can render the rest of the word, write the next letter. else, skip to the next line. // TODO(raidancampbell): optimize this to write out the whole word, this will involve referencing the // above special cases if counter+(j-i) <= width+1 && !(j == i && counter == width+1) { content.WriteRune(ch) counter++ } else if ch == ' ' || ch == '\t' { // we want to wrap and write this char, but it's a space. eat it to prevent the next line from // having a leading whitespace because of our wrapping counter++ } else { content.WriteRune('\n') counter = 0 content.WriteString(spacer) counter += len(spacer) content.WriteRune(ch) counter++ } } } p.WrappedContent = strings.Split(content.String(), "\n") p.WrapWidth = width p.Color = color p.HighlightFoundText() } func (p *Page) HighlightFoundText() { if p.SearchTerm == "" { return } for i, ln := range p.WrappedContent { found := strings.Index(ln, p.SearchTerm) if found < 0 { continue } format := "\033[7m%s\033[27m" if bombadillo.Options["theme"] == "inverse" { format = "\033[27m%s\033[7m" } ln = strings.Replace(ln, p.SearchTerm, fmt.Sprintf(format, p.SearchTerm), -1) p.WrappedContent[i] = ln } } func (p *Page) FindText() { p.FoundLinkLines = make([]int, 0, 10) s := p.SearchTerm p.SearchIndex = 0 if s == "" { return } format := "\033[7m%s\033[27m" if bombadillo.Options["theme"] == "inverse" { format = "\033[27m%s\033[7m" } for i, ln := range p.WrappedContent { found := strings.Index(ln, s) if found < 0 { continue } ln = strings.Replace(ln, s, fmt.Sprintf(format, s), -1) p.WrappedContent[i] = ln p.FoundLinkLines = append(p.FoundLinkLines, i) } } //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ // MakePage returns a Page struct with default values func MakePage(url Url, content string, links []string) Page { p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0, "", 40, false} return p } func min(a, b int) int { if a < b { return a } return b } bombadillo/main.go0000644000175000017500000001441414211226262013765 0ustar nileshnileshpackage main // Bombadillo is an internet client for the terminal of unix or // unix-like systems. // // Copyright (C) 2019 Brian Evans // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . import ( "flag" "fmt" "io/ioutil" "os" "os/signal" "path/filepath" "strconv" "strings" "syscall" "time" "tildegit.org/sloum/bombadillo/config" "tildegit.org/sloum/bombadillo/cui" "tildegit.org/sloum/bombadillo/gemini" ) var version string = "2.3.3" var bombadillo *client var helplocation string = "gopher://bombadillo.colorfield.space:70/1/user-guide.map" var settings config.Config func saveConfig() error { var opts strings.Builder bkmrks := bombadillo.BookMarks.IniDump() certs := bombadillo.Certs.IniDump() opts.WriteString("\n[SETTINGS]\n") for k, v := range bombadillo.Options { opts.WriteString(k) opts.WriteRune('=') opts.WriteString(v) opts.WriteRune('\n') } opts.WriteString(bkmrks) opts.WriteString(certs) return ioutil.WriteFile(filepath.Join(bombadillo.Options["configlocation"], ".bombadillo.ini"), []byte(opts.String()), 0644) } func validateOpt(opt, val string) bool { var validOpts = map[string][]string{ "webmode": []string{"none", "gui", "lynx", "w3m", "elinks"}, "theme": []string{"normal", "inverse", "color"}, "defaultscheme": []string{"gopher", "gemini", "http", "https"}, "showimages": []string{"true", "false"}, "geminiblocks": []string{"block", "neither", "alt", "both"}, } opt = strings.ToLower(opt) val = strings.ToLower(val) if _, ok := validOpts[opt]; ok { for _, item := range validOpts[opt] { if item == val { return true } } return false } if opt == "timeout" { _, err := strconv.Atoi(val) if err != nil { return false } } return true } func lowerCaseOpt(opt, val string) string { switch opt { case "webmode", "theme", "defaultscheme", "showimages", "geminiblocks": return strings.ToLower(val) default: return val } } func loadConfig() { err := os.MkdirAll(bombadillo.Options["configlocation"], 0755) if err != nil { exitMsg := fmt.Sprintf("Error creating 'configlocation' directory: %s", err.Error()) cui.Exit(3, exitMsg) } fp := filepath.Join(bombadillo.Options["configlocation"], ".bombadillo.ini") file, err := os.Open(fp) if err != nil { err = saveConfig() if err != nil { exitMsg := fmt.Sprintf("Error writing config file during bootup: %s", err.Error()) cui.Exit(4, exitMsg) } } confparser := config.NewParser(file) settings, _ = confparser.Parse() _ = file.Close() for _, v := range settings.Settings { lowerkey := strings.ToLower(v.Key) if lowerkey == "configlocation" { // Read only continue } if _, ok := bombadillo.Options[lowerkey]; ok { if validateOpt(lowerkey, v.Value) { bombadillo.Options[lowerkey] = v.Value if lowerkey == "geminiblocks" { gemini.BlockBehavior = v.Value } else if lowerkey == "timeout" { updateTimeouts(v.Value) } } else { bombadillo.Options[lowerkey] = defaultOptions[lowerkey] } } } for i, v := range settings.Bookmarks.Titles { _, _ = bombadillo.BookMarks.Add([]string{v, settings.Bookmarks.Links[i]}) } for _, v := range settings.Certs { // Remove expired certs vals := strings.SplitN(v.Value, "|", -1) if len(vals) < 2 { continue } now := time.Now() ts, err := strconv.ParseInt(vals[1], 10, 64) if err != nil || now.Unix() > ts { continue } // Satisfied that the cert is not expired // or malformed: add to the current client // instance bombadillo.Certs.Add(v.Key, vals[0], ts) } } func initClient() { bombadillo = MakeClient(" ((( Bombadillo ))) ") loadConfig() } // In the event of specific signals, ensure the display is shown correctly. // Accepts a signal, blocking until it is received. Once not blocked, corrects // terminal display settings as appropriate for that signal. Loops // indefinitely, does not return. func handleSignals(c <-chan os.Signal) { for { switch <-c { case syscall.SIGTSTP: cui.CleanupTerm() _ = syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) case syscall.SIGCONT: cui.InitTerm() bombadillo.Draw() case syscall.SIGINT: cui.Exit(130, "") } } } //printHelp produces a nice display message when the --help flag is used func printHelp() { art := `Bombadillo - a non-web browser Syntax: bombadillo [options] [url] Examples: bombadillo gopher://bombadillo.colorfield.space bombadillo -t bombadillo -v Options: ` _, _ = fmt.Fprint(os.Stdout, art) flag.PrintDefaults() } func main() { getVersion := flag.Bool("v", false, "Display version information and exit") addTitleToXWindow := flag.Bool("t", false, "Set the window title to 'Bombadillo'. Can be used in a GUI environment, however not all terminals support this feature.") flag.Usage = printHelp flag.Parse() if *getVersion { fmt.Printf("Bombadillo %s\n", version) os.Exit(0) } args := flag.Args() cui.InitTerm() if *addTitleToXWindow { fmt.Print("\033[22;0t") // Store window title on terminal stack fmt.Print("\033]0;Bombadillo\007") // Update window title } defer cui.Exit(0, "") initClient() // watch for signals, send them to be handled c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTSTP, syscall.SIGCONT, syscall.SIGINT) go handleSignals(c) // Start polling for terminal size changes go bombadillo.GetSize() if len(args) > 0 { // If a url was passed, move it down the line // Goroutine so keypresses can be made during // page load bombadillo.Visit(args[0]) } else { // Otherwise, load the homeurl // Goroutine so keypresses can be made during // page load bombadillo.Visit(bombadillo.Options["homeurl"]) } // Loop indefinitely on user input for { bombadillo.TakeControlInput() } } bombadillo/local/0000755000175000017500000000000014211226262013600 5ustar nileshnileshbombadillo/local/local.go0000644000175000017500000000355014211226262015224 0ustar nileshnileshpackage local import ( "fmt" "io/ioutil" "os" "path/filepath" "sort" "strings" ) func Open(address string) (string, []string, error) { links := make([]string, 0, 10) if !pathExists(address) { return "", links, fmt.Errorf("Invalid system path: %s", address) } file, err := os.Open(address) if err != nil { return "", links, err } defer file.Close() if pathIsDir(address) { offset := 1 fileList, err := file.Readdir(0) if err != nil { return "", links, err } var out strings.Builder out.WriteString(fmt.Sprintf("Current directory: %s\n\n", address)) // Handle 'addres/..' display offset = 2 upFp := filepath.Join(address, "..") upOneLevel, _ := filepath.Abs(upFp) info, err := os.Stat(upOneLevel) if err == nil { out.WriteString("[1] ") out.WriteString(fmt.Sprintf("%-12s ", info.Mode().String())) out.WriteString("../\n") links = append(links, upOneLevel) } // Sort the directory contents alphabetically sort.Slice(fileList, func(i, j int) bool { return fileList[i].Name() < fileList[j].Name() }) // Handle each item in the directory for i, obj := range fileList { linkNum := fmt.Sprintf("[%d]", i+offset) out.WriteString(fmt.Sprintf("%-5s ", linkNum)) out.WriteString(fmt.Sprintf("%-12s ", obj.Mode().String())) out.WriteString(obj.Name()) if obj.IsDir() { out.WriteString("/") } out.WriteString("\n") fp := filepath.Join(address, obj.Name()) links = append(links, fp) } return out.String(), links, nil } bytes, err := ioutil.ReadAll(file) if err != nil { return "", links, err } return string(bytes), links, nil } func pathExists(p string) bool { exists := true if _, err := os.Stat(p); os.IsNotExist(err) { exists = false } return exists } func pathIsDir(p string) bool { info, err := os.Stat(p) if err != nil { return false } return info.IsDir() } bombadillo/http/0000755000175000017500000000000014211226262013465 5ustar nileshnileshbombadillo/http/open_browser_windows.go0000644000175000017500000000051414211226262020272 0ustar nileshnilesh// This will only build for windows based on the filename // no build tag required package http import "os/exec" func OpenInBrowser(url string) (string, error) { err := exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() if err != nil { return "", err } return "Opened in system default web browser", nil } bombadillo/http/open_browser_other.go0000644000175000017500000000145314211226262017724 0ustar nileshnilesh// +build !darwin,!windows package http import ( "fmt" "os" "os/exec" ) // OpenInBrowser checks for the presence of a display server // and environment variables indicating a gui is present. If found // then xdg-open is called on a url to open said url in the default // gui web browser for the system func OpenInBrowser(url string) (string, error) { disp := os.Getenv("DISPLAY") wayland := os.Getenv("WAYLAND_DISPLAY") _, err := exec.LookPath("Xorg") if disp == "" && wayland == "" && err != nil { return "", fmt.Errorf("No gui is available, check 'webmode' setting") } // Use start rather than run or output in order // to release the process and not block err = exec.Command("xdg-open", url).Start() if err != nil { return "", err } return "Opened in system default web browser", nil } bombadillo/http/open_browser_darwin.go0000644000175000017500000000043414211226262020065 0ustar nileshnilesh// This will build for osx without a build tag based on the filename package http import "os/exec" func OpenInBrowser(url string) (string, error) { err := exec.Command("open", url).Start() if err != nil { return "", err } return "Opened in system default web browser", nil } bombadillo/http/http_render.go0000644000175000017500000000423014211226262016331 0ustar nileshnileshpackage http import ( "fmt" "io/ioutil" "net/http" "os/exec" "strings" ) // Page represents the contents and links or an http/https document type Page struct { Content string Links []string } // Visit is the main entry to viewing a web document in bombadillo. // It takes a url, a terminal width, and which web backend the user // currently has set. Visit returns a Page and an error func Visit(webmode, url string, width int) (Page, error) { if width > 80 { width = 80 } var w string switch webmode { case "lynx": w = "-width" case "w3m": w = "-cols" case "elinks": w = "-dump-width" default: return Page{}, fmt.Errorf("Invalid webmode setting") } c, err := exec.Command(webmode, "-dump", w, fmt.Sprintf("%d", width), url).Output() if err != nil && c == nil { return Page{}, err } return parseLinks(string(c)), nil } // IsTextFile makes an http(s) head request to a given URL // and determines if the content-type is text based. It then // returns a bool func IsTextFile(url string) bool { resp, err := http.Head(url) if err != nil { return false } ctype := resp.Header.Get("content-type") if strings.Contains(ctype, "text") || ctype == "" { return true } return false } func parseLinks(c string) Page { var out Page contentUntil := strings.LastIndex(c, "References") if contentUntil >= 1 { out.Content = c[:contentUntil] } else { out.Content = c out.Links = make([]string, 0) return out } links := c[contentUntil+11:] links = strings.TrimSpace(links) linkSlice := strings.Split(links, "\n") out.Links = make([]string, 0, len(linkSlice)) for _, link := range linkSlice { ls := strings.SplitN(link, ".", 2) if len(ls) < 2 { continue } out.Links = append(out.Links, strings.TrimSpace(ls[1])) } return out } // Fetch makes an http(s) request and returns the []bytes // for the response and an error. Fetch is used for saving // the source file of an http(s) document func Fetch(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err } defer resp.Body.Close() bodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { return []byte{}, err } return bodyBytes, nil } bombadillo/help.go0000644000175000017500000000176114211226262013772 0ustar nileshnileshpackage main // ERRS maps commands to their syntax error message var ERRS = map[string]string{ "A": "`a [target] [name...]`", "ADD": "`add [target] [name...]`", "D": "`d [bookmark-id]`", "DELETE": "`delete [bookmark-id]`", "B": "`b [[bookmark-id]]`", "BOOKMARKS": "`bookmarks [[bookmark-id]]`", "C": "`c [link_id]` or `c [setting]`", "CHECK": "`check [link_id]` or `check [setting]`", "H": "`h`", "HOME": "`home`", "J": "`j [[history_position]]`", "JUMP": "`jump [[history_position]]`", "P": "`p [host]`", "PURGE": "`purge [host]`", "Q": "`q`", "QUIT": "`quit`", "R": "`r`", "RELOAD": "`reload`", "SEARCH": "`search [[keyword(s)...]]`", "S": "`s [setting] [value]`", "SET": "`set [setting] [value]`", "W": "`w [target]`", "WRITE": "`write [target]`", "VERSION": "`version`", "?": "`? [[command]]`", "HELP": "`help [[command]]`", } bombadillo/headbar.go0000644000175000017500000000227014211226262014424 0ustar nileshnileshpackage main import ( "fmt" ) //------------------------------------------------\\ // + + + T Y P E S + + + \\ //--------------------------------------------------\\ // Headbar represents the contents of the top bar of // the client and contains the client name and the // current URL type Headbar struct { title string url string } //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ // Render returns a string with the contents of theHeadbar func (h *Headbar) Render(width int, theme string) string { maxMsgWidth := width - len([]rune(h.title)) - 2 if theme == "inverse" { return fmt.Sprintf("\033[7m%s▟\033[27m %-*.*s\033[0m", h.title, maxMsgWidth, maxMsgWidth, h.url) } return fmt.Sprintf("%s▟\033[7m %-*.*s\033[0m", h.title, maxMsgWidth, maxMsgWidth, h.url) } //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ // MakeHeadbar returns a Headbar with default values func MakeHeadbar(title string) Headbar { return Headbar{title, ""} } bombadillo/gopher/0000755000175000017500000000000014211226262013772 5ustar nileshnileshbombadillo/gopher/gopher.go0000644000175000017500000001041414211226262015605 0ustar nileshnilesh// Contains the building blocks of a gopher client: history, url, and view. // History handles the browsing session and view represents individual // text based resources, the url represents a parsed url. package gopher import ( "errors" "fmt" "io/ioutil" "net" "strings" "time" ) //------------------------------------------------\\ // + + + V A R I A B L E S + + + \\ //--------------------------------------------------\\ // types is a map of gophertypes to a string representing their // type, to be used when displaying gophermaps var types = map[string]string{ "0": "TXT", "1": "MAP", "3": "ERR", "4": "BIN", "5": "DOS", "6": "UUE", "7": "FTS", "8": "TEL", "9": "BIN", "g": "GIF", "G": "GEM", "h": "HTM", "I": "IMG", "p": "PNG", "s": "SND", "S": "SSH", "T": "TEL", } var Timeout time.Duration = time.Duration(15) * time.Second //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ // Retrieve makes a request to a Url and resturns // the response as []byte/error. This function is // available to use directly, but in most implementations // using the "Visit" receiver of the History struct will // be better. func Retrieve(host, port, resource string) ([]byte, error) { nullRes := make([]byte, 0) if host == "" || port == "" { return nullRes, errors.New("Incomplete request url") } addr := host + ":" + port conn, err := net.DialTimeout("tcp", addr, Timeout) if err != nil { return nullRes, err } send := resource + "\n" _, err = conn.Write([]byte(send)) if err != nil { return nullRes, err } result, err := ioutil.ReadAll(conn) if err != nil { return nullRes, err } return result, nil } // Visit handles the making of the request, parsing of maps, and returning // the correct information to the client func Visit(gophertype, host, port, resource string) (string, []string, error) { resp, err := Retrieve(host, port, resource) if err != nil { return "", []string{}, err } text := string(resp) links := []string{} if IsDownloadOnly(gophertype) { return text, []string{}, nil } if gophertype == "1" { text, links = parseMap(text) } return text, links, nil } func getType(t string) string { if val, ok := types[t]; ok { return val } return "???" } func isWebLink(resource string) (string, bool) { split := strings.SplitN(resource, ":", 2) if first := strings.ToUpper(split[0]); first == "URL" && len(split) > 1 { return split[1], true } return "", false } func parseMap(text string) (string, []string) { splitContent := strings.Split(text, "\n") links := make([]string, 0, 10) for i, e := range splitContent { e = strings.Trim(e, "\r\n") if e == "." { splitContent[i] = "" continue } line := strings.Split(e, "\t") var title string if len(line[0]) > 1 { title = line[0][1:] } else if len(line[0]) == 1 { title = "" } else { title = "" line[0] = "i" } if len(line) < 4 || strings.HasPrefix(line[0], "i") { splitContent[i] = " " + string(title) } else { link := buildLink(line[2], line[3], string(line[0][0]), line[1]) links = append(links, link) linkNum := fmt.Sprintf("[%d]",len(links)) linktext := fmt.Sprintf("%s %5s %s", getType(string(line[0][0])), linkNum, title) splitContent[i] = linktext } } return strings.Join(splitContent, "\n"), links } // Returns false for all text formats (including html // even though it may link out. Things like telnet // should never make it into the retrieve call for // this module, having been handled in the client // based on their protocol. func IsDownloadOnly(gophertype string) bool { switch gophertype { case "0", "1", "3", "7", "h": return false default: return true } } func buildLink(host, port, gtype, resource string) string { switch gtype { case "8", "T": return fmt.Sprintf("telnet://%s:%s", host, port) case "G": return fmt.Sprintf("gemini://%s:%s%s", host, port, resource) case "h": u, tf := isWebLink(resource) if tf { if strings.Index(u, "://") > 0 { return u } else { return fmt.Sprintf("http://%s", u) } } return fmt.Sprintf("gopher://%s:%s/h%s", host, port, resource) default: return fmt.Sprintf("gopher://%s:%s/%s%s", host, port, gtype, resource) } } bombadillo/go.mod0000644000175000017500000000005614211226262013615 0ustar nileshnileshmodule tildegit.org/sloum/bombadillo go 1.11 bombadillo/gemini/0000755000175000017500000000000014211226262013756 5ustar nileshnileshbombadillo/gemini/gemini.go0000644000175000017500000002657114211226262015570 0ustar nileshnileshpackage gemini import ( "bytes" "crypto/sha1" "crypto/tls" "fmt" "io/ioutil" "net" "net/url" "strconv" "strings" "time" ) type Capsule struct { MimeMaj string MimeMin string Status int Content string Links []string } type TofuDigest struct { certs map[string]string } var BlockBehavior string = "block" var TlsTimeout time.Duration = time.Duration(15) * time.Second //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ func (t *TofuDigest) Purge(host string) error { host = strings.ToLower(host) if host == "*" { t.certs = make(map[string]string) return nil } else if _, ok := t.certs[strings.ToLower(host)]; ok { delete(t.certs, host) return nil } return fmt.Errorf("Invalid host %q", host) } func (t *TofuDigest) Add(host, hash string, time int64) { t.certs[strings.ToLower(host)] = fmt.Sprintf("%s|%d", hash, time) } func (t *TofuDigest) Exists(host string) bool { if _, ok := t.certs[strings.ToLower(host)]; ok { return true } return false } func (t *TofuDigest) Find(host string) (string, error) { if hash, ok := t.certs[strings.ToLower(host)]; ok { return hash, nil } return "", fmt.Errorf("Invalid hostname, no key saved") } func (t *TofuDigest) Match(host, localCert string, cState *tls.ConnectionState) error { now := time.Now() for _, cert := range cState.PeerCertificates { if localCert != hashCert(cert.Raw) { continue } if now.Before(cert.NotBefore) { return fmt.Errorf("Certificate is not valid yet") } if now.After(cert.NotAfter) { return fmt.Errorf("EXP") } if err := cert.VerifyHostname(host); err != nil && cert.Subject.CommonName != host { return fmt.Errorf("Certificate error: %s", err) } return nil } return fmt.Errorf("No matching certificate was found for host %q", host) } func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error { host = strings.ToLower(host) now := time.Now() var reasons strings.Builder for index, cert := range cState.PeerCertificates { if index > 0 { reasons.WriteString("; ") } if now.Before(cert.NotBefore) { reasons.WriteString(fmt.Sprintf("Cert [%d] is not valid yet", index+1)) continue } if now.After(cert.NotAfter) { reasons.WriteString(fmt.Sprintf("Cert [%d] is expired", index+1)) continue } if err := cert.VerifyHostname(host); err != nil && cert.Subject.CommonName != host { reasons.WriteString(fmt.Sprintf("Cert [%d] hostname does not match", index+1)) continue } t.Add(host, hashCert(cert.Raw), cert.NotAfter.Unix()) return nil } return fmt.Errorf(reasons.String()) } func (t *TofuDigest) GetCertAndTimestamp(host string) (string, int64, error) { certTs, err := t.Find(host) if err != nil { return "", -1, err } certTsSplit := strings.SplitN(certTs, "|", -1) if len(certTsSplit) < 2 { _ = t.Purge(host) return certTsSplit[0], -1, fmt.Errorf("Invalid certstring, no delimiter") } ts, err := strconv.ParseInt(certTsSplit[1], 10, 64) if err != nil { _ = t.Purge(host) return certTsSplit[0], -1, err } now := time.Now() if ts < now.Unix() { // Ignore error return here since an error would indicate // the host does not exist and we have already checked for // that and the desired outcome of the action is that the // host will no longer exist, so we are good either way _ = t.Purge(host) return "", -1, fmt.Errorf("Expired cert") } return certTsSplit[0], ts, nil } func (t *TofuDigest) IniDump() string { if len(t.certs) < 1 { return "" } var out strings.Builder out.WriteString("[CERTS]\n") for k, v := range t.certs { out.WriteString(k) out.WriteString("=") out.WriteString(v) out.WriteString("\n") } return out.String() } //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) { if host == "" || port == "" { return "", fmt.Errorf("Incomplete request url") } addr := host + ":" + port conf := &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: true, } conn, err := tls.DialWithDialer(&net.Dialer{Timeout: TlsTimeout}, "tcp", addr, conf) if err != nil { return "", fmt.Errorf("TLS Dial Error: %s", err.Error()) } defer conn.Close() connState := conn.ConnectionState() // Begin TOFU screening... // If no certificates are offered, bail out if len(connState.PeerCertificates) < 1 { return "", fmt.Errorf("Insecure, no certificates offered by server") } localCert, localTs, err := td.GetCertAndTimestamp(host) if localTs > 0 { // See if we have a matching cert err := td.Match(host, localCert, &connState) if err != nil && err.Error() != "EXP" { // If there is no match and it isnt because of an expiration // just return the error return "", err } else if err != nil { // The cert expired, see if they are offering one that is valid... err := td.newCert(host, &connState) if err != nil { // If there are no valid certs to offer, let the client know return "", err } } } else { err = td.newCert(host, &connState) if err != nil { // If there are no valid certs to offer, let the client know return "", err } } send := "gemini://" + addr + "/" + resource + "\r\n" _, err = conn.Write([]byte(send)) if err != nil { return "", err } result, err := ioutil.ReadAll(conn) if err != nil { return "", err } return string(result), nil } func Fetch(host, port, resource string, td *TofuDigest) ([]byte, error) { rawResp, err := Retrieve(host, port, resource, td) if err != nil { return make([]byte, 0), err } resp := strings.SplitN(rawResp, "\r\n", 2) if len(resp) != 2 { if err != nil { return make([]byte, 0), fmt.Errorf("Invalid response from server") } } header := strings.SplitN(resp[0], " ", 2) if len([]rune(header[0])) != 2 { header = strings.SplitN(resp[0], "\t", 2) if len([]rune(header[0])) != 2 { return make([]byte, 0), fmt.Errorf("Invalid response format from server") } } // Get status code single digit form status, err := strconv.Atoi(string(header[0][0])) if err != nil { return make([]byte, 0), fmt.Errorf("Invalid status response from server") } if status != 2 { switch status { case 1: return make([]byte, 0), fmt.Errorf("[1] Queries cannot be saved.") case 3: return make([]byte, 0), fmt.Errorf("[3] Redirects cannot be saved.") case 4: return make([]byte, 0), fmt.Errorf("[4] Temporary Failure.") case 5: return make([]byte, 0), fmt.Errorf("[5] Permanent Failure.") case 6: return make([]byte, 0), fmt.Errorf("[6] Client Certificate Required (Unsupported)") default: return make([]byte, 0), fmt.Errorf("Invalid response status from server") } } return []byte(resp[1]), nil } func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) { capsule := MakeCapsule() rawResp, err := Retrieve(host, port, resource, td) if err != nil { return capsule, err } resp := strings.SplitN(rawResp, "\r\n", 2) if len(resp) != 2 { if err != nil { return capsule, fmt.Errorf("Invalid response from server") } } header := strings.SplitN(resp[0], " ", 2) if len([]rune(header[0])) != 2 { header = strings.SplitN(resp[0], "\t", 2) if len([]rune(header[0])) != 2 { return capsule, fmt.Errorf("Invalid response format from server") } } body := resp[1] // Get status code single digit form capsule.Status, err = strconv.Atoi(string(header[0][0])) if err != nil { return capsule, fmt.Errorf("Invalid status response from server") } // Parse the meta as needed var meta string switch capsule.Status { case 1: capsule.Content = header[1] return capsule, nil case 2: mimeAndCharset := strings.Split(header[1], ";") meta = mimeAndCharset[0] if meta == "" { meta = "text/gemini" } minMajMime := strings.Split(meta, "/") if len(minMajMime) < 2 { return capsule, fmt.Errorf("Improperly formatted mimetype received from server") } capsule.MimeMaj = minMajMime[0] capsule.MimeMin = minMajMime[1] if capsule.MimeMaj == "text" && capsule.MimeMin == "gemini" { if len(resource) > 0 && resource[0] != '/' { resource = fmt.Sprintf("/%s", resource) } else if resource == "" { resource = "/" } currentUrl := fmt.Sprintf("gemini://%s:%s%s", host, port, resource) capsule.Content, capsule.Links = parseGemini(body, currentUrl) } else { capsule.Content = body } return capsule, nil case 3: // The client will handle informing the user of a redirect // and then request the new url capsule.Content = header[1] return capsule, nil case 4: return capsule, fmt.Errorf("[4] Temporary Failure. %s", header[1]) case 5: return capsule, fmt.Errorf("[5] Permanent Failure. %s", header[1]) case 6: return capsule, fmt.Errorf("[6] Client Certificate Required (Unsupported)") default: return capsule, fmt.Errorf("Invalid response status from server") } } func parseGemini(b, currentUrl string) (string, []string) { splitContent := strings.Split(b, "\n") links := make([]string, 0, 10) inPreBlock := false spacer := " " outputIndex := 0 for i, ln := range splitContent { splitContent[i] = strings.Trim(ln, "\r\n") isPreBlockDeclaration := strings.HasPrefix(ln, "```") if isPreBlockDeclaration && !inPreBlock && (BlockBehavior == "both" || BlockBehavior == "alt") { inPreBlock = !inPreBlock alt := strings.TrimSpace(ln) if len(alt) > 3 { alt = strings.TrimSpace(alt[3:]) splitContent[outputIndex] = fmt.Sprintf("%s[ALT][ %s ]", spacer, alt) outputIndex++ } } else if isPreBlockDeclaration { inPreBlock = !inPreBlock } else if len([]rune(ln)) > 3 && ln[:2] == "=>" && !inPreBlock { var link, decorator string subLn := strings.Trim(ln[2:], "\r\n\t \a") splitPoint := strings.IndexAny(subLn, " \t") if splitPoint < 0 || len([]rune(subLn))-1 <= splitPoint { link = subLn decorator = subLn } else { link = strings.Trim(subLn[:splitPoint], "\t\n\r \a") decorator = strings.Trim(subLn[splitPoint:], "\t\n\r \a") } if strings.Index(link, "://") < 0 { link, _ = HandleRelativeUrl(link, currentUrl) } links = append(links, link) linknum := fmt.Sprintf("[%d]", len(links)) splitContent[outputIndex] = fmt.Sprintf("%-5s %s", linknum, decorator) outputIndex++ } else { if inPreBlock && (BlockBehavior == "alt" || BlockBehavior == "neither") { continue } var leader, tail string = "", "" if len(ln) > 0 && ln[0] == '#' { leader = "\033[1m" tail = "\033[0m" } splitContent[outputIndex] = fmt.Sprintf("%s%s%s%s", spacer, leader, ln, tail) outputIndex++ } } return strings.Join(splitContent[:outputIndex], "\n"), links } // handleRelativeUrl provides link completion func HandleRelativeUrl(relLink, current string) (string, error) { base, err := url.Parse(current) if err != nil { return relLink, err } rel, err := url.Parse(relLink) if err != nil { return relLink, err } return base.ResolveReference(rel).String(), nil } func hashCert(cert []byte) string { hash := sha1.Sum(cert) hex := make([][]byte, len(hash)) for i, data := range hash { hex[i] = []byte(fmt.Sprintf("%02X", data)) } return fmt.Sprintf("%s", string(bytes.Join(hex, []byte(":")))) } func MakeCapsule() Capsule { return Capsule{"", "", 0, "", make([]string, 0, 5)} } func MakeTofuDigest() TofuDigest { return TofuDigest{make(map[string]string)} } bombadillo/footbar.go0000644000175000017500000000276114211226262014477 0ustar nileshnileshpackage main import ( "fmt" "strconv" ) //------------------------------------------------\\ // + + + T Y P E S + + + \\ //--------------------------------------------------\\ // Footbar deals with the values present in the // client's footbar type Footbar struct { PercentRead string PageType string } //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ // SetPercentRead sets the percentage of the current // document the user has read func (f *Footbar) SetPercentRead(p int) { if p > 100 { p = 100 } else if p < 0 { p = 0 } f.PercentRead = strconv.Itoa(p) + "%" } // SetPageType sets the current page's type // NOTE: This is not currently in use func (f *Footbar) SetPageType(t string) { f.PageType = t } // Render returns a string representing the visual display // of the bookmarks bar func (f *Footbar) Render(termWidth, position int, theme string) string { pre := fmt.Sprintf("HST: (%2.2d) - - - %4s Read ", position+1, f.PercentRead) out := "\033[0m%*.*s " if theme == "inverse" { out = "\033[7m%*.*s \033[0m" } return fmt.Sprintf(out, termWidth-1, termWidth-1, pre) } //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ // MakeFootbar returns a footbar with default values func MakeFootbar() Footbar { return Footbar{"---", "N/A"} } bombadillo/finger/0000755000175000017500000000000014211226262013760 5ustar nileshnileshbombadillo/finger/finger.go0000644000175000017500000000077514211226262015572 0ustar nileshnileshpackage finger import ( "fmt" "io/ioutil" "net" "time" ) func Finger(host, port, resource string) (string, error) { addr := fmt.Sprintf("%s:%s", host, port) timeOut := time.Duration(3) * time.Second conn, err := net.DialTimeout("tcp", addr, timeOut) if err != nil { return "", err } defer conn.Close() _, err = conn.Write([]byte(resource + "\r\n")) if err != nil { return "", err } result, err := ioutil.ReadAll(conn) if err != nil { return "", err } return string(result), nil } bombadillo/defaults.go0000644000175000017500000000543514211226262014653 0ustar nileshnileshpackage main import ( "os" "os/user" "path/filepath" ) var defaultOptions = map[string]string{ // The configuration options below control the default settings for // users of Bombadillo. // // Changes take effect when Bombadillo is built. Follow the standard // install instructions after making a change. // // Most options can be changed by a user in the Bombadillo client, and // changes made here will not overwrite an existing user's settings. // The exception to both cases is "configlocation" which controls where // .bombadillo.ini is stored. If you make changes to this setting, // consider moving bombadillo.ini to the new location as well, so you // (or your users) do not loose bookmarks or other preferences. // // Further explanation of each option is available in the man page. // Basic Usage // // Any option can be defined as a string, like this: // "option": "value" // // Options can also have values calculated on startup. There are two // functions below that do just this: homePath() and xdgConfigPath() // You can set any value to use these functions like this: // "option": homePath() // "option": xdgConfigPath() // See the comments for these functions for more information on what // they do. // // You can also use `filepath.Join()` if you want to build a file path. // For example, specify "~/bombadillo" like so: // "option": filepath.Join(homePath(), bombadillo) // Moving .bombadillo.ini out of your home directory // // To ensure .bombadillo.ini is saved as per XDG config spec, change // the "configlocation" as follows: // "configlocation": xdgConfigPath() "configlocation": xdgConfigPath(), "defaultscheme": "gopher", // "gopher", "gemini", "http", "https" "geminiblocks": "block", // "block", "alt", "neither", "both" "homeurl": "gopher://bombadillo.colorfield.space:70/1/user-guide.map", "savelocation": homePath(), "searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs", "showimages": "true", "telnetcommand": "telnet", "theme": "normal", // "normal", "inverted", "color" "timeout": "15", // connection timeout for gopher/gemini in seconds "webmode": "none", // "none", "gui", "lynx", "w3m", "elinks" "maxwidth": "100", } // homePath will return the path to your home directory as a string // Usage: // "configlocation": homeConfigPath() func homePath() string { var userinfo, _ = user.Current() return userinfo.HomeDir } // xdgConfigPath returns the path to your XDG base directory for configuration // i.e the contents of environment variable XDG_CONFIG_HOME, or ~/.config/ // Usage: // "configlocation": xdgConfigPath() func xdgConfigPath() string { configPath := os.Getenv("XDG_CONFIG_HOME") if configPath == "" { return filepath.Join(homePath(), ".config") } return configPath } bombadillo/cui/0000755000175000017500000000000014211226262013266 5ustar nileshnileshbombadillo/cui/cui.go0000644000175000017500000000474714211226262014411 0ustar nileshnileshpackage cui import ( "bufio" "fmt" "os" "os/exec" "tildegit.org/sloum/bombadillo/termios" ) var Shapes = map[string]string{ "walll": "╎", "wallr": " ", "ceiling": " ", "floor": " ", "tl": "╎", "tr": " ", "bl": "╎", "br": " ", "awalll": "▌", "awallr": "▐", "aceiling": "▀", "afloor": "▄", "atl": "▞", "atr": "▜", "abl": "▚", "abr": "▟", } 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) } } // Exit performs cleanup operations before exiting the application func Exit(exitCode int, msg string) { CleanupTerm() if msg != "" { fmt.Print(msg, "\n") } fmt.Print("\033[23;0t") // Restore window title from terminal stack os.Exit(exitCode) } // InitTerm sets the terminal modes appropriate for Bombadillo func InitTerm() { termios.SetCharMode() Tput("smcup") // use alternate screen Tput("rmam") // turn off line wrapping fmt.Print("\033[?25l") // hide cursor } // CleanupTerm reverts changs to terminal mode made by InitTerm func CleanupTerm() { moveCursorToward("down", 500) moveCursorToward("right", 500) termios.SetLineMode() fmt.Print("\n") fmt.Print("\033[?25h") // reenables cursor blinking Tput("smam") // turn on line wrap Tput("rmcup") // stop using alternate screen } 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 Getch() rune { reader := bufio.NewReader(os.Stdin) char, _, err := reader.ReadRune() if err != nil { return '@' } return char } func GetLine(prefix string) (string, error) { termios.SetLineMode() defer termios.SetCharMode() reader := bufio.NewReader(os.Stdin) fmt.Print(prefix) text, err := reader.ReadString('\n') if err != nil { return "", err } return text[:len(text)-1], nil } func Tput(opt string) { cmd := exec.Command("tput", opt) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout // explicitly ignoring the error here as // the alternate screen is an optional feature // that may not be available everywhere we expect // to run _ = cmd.Run() } bombadillo/config/0000755000175000017500000000000014211226262013753 5ustar nileshnileshbombadillo/config/parser.go0000644000175000017500000000451514211226262015603 0ustar nileshnileshpackage config import ( "fmt" "io" "strings" ) //------------------------------------------------\\ // + + + T Y P E S + + + \\ //--------------------------------------------------\\ type Parser struct { s *scanner row int buffer struct { token Token size int } } type Config struct { // Bookmarks gopher.Bookmarks Bookmarks struct { Titles, Links []string } Settings []KeyValue Certs []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.Titles = append(c.Bookmarks.Titles, keyval.Value) c.Bookmarks.Links = append(c.Bookmarks.Links, keyval.Key) case "CERTS": c.Certs = append(c.Certs, 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)} } bombadillo/config/lexer.go0000644000175000017500000000757114211226262015433 0ustar nileshnileshpackage config import ( "bufio" "bytes" "fmt" "io" ) //------------------------------------------------\\ // + + + 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 } bombadillo/cmdparse/0000755000175000017500000000000014211226262014304 5ustar nileshnileshbombadillo/cmdparse/parser.go0000644000175000017500000000476714211226262016145 0ustar nileshnileshpackage cmdparse import ( "fmt" "io" ) //------------------------------------------------\\ // + + + 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 DO ) //------------------------------------------------\\ // + + + 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, Action: cm.Value = append(cm.Value, t.val) cm.Type = DO case 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)} } bombadillo/cmdparse/lexer.go0000644000175000017500000000547014211226262015760 0ustar nileshnileshpackage cmdparse import ( "bufio" "bytes" "io" "strings" ) //------------------------------------------------\\ // + + + 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 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 "D", "DELETE", "A", "ADD", "W", "WRITE", "S", "SET", "R", "RELOAD", "SEARCH", "Q", "QUIT", "B", "BOOKMARKS", "H", "HOME", "?", "HELP", "C", "CHECK", "P", "PURGE", "JUMP", "J", "VERSION": 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' } bombadillo/client.go0000644000175000017500000007640514211226262014327 0ustar nileshnileshpackage main import ( "fmt" "io/ioutil" "net/url" "os" "path/filepath" "regexp" "strconv" "strings" "time" "tildegit.org/sloum/bombadillo/cmdparse" "tildegit.org/sloum/bombadillo/cui" "tildegit.org/sloum/bombadillo/finger" "tildegit.org/sloum/bombadillo/gemini" "tildegit.org/sloum/bombadillo/gopher" "tildegit.org/sloum/bombadillo/http" "tildegit.org/sloum/bombadillo/local" "tildegit.org/sloum/bombadillo/telnet" "tildegit.org/sloum/bombadillo/termios" ) //------------------------------------------------\\ // + + + T Y P E S + + + \\ //--------------------------------------------------\\ type client struct { Height int Width int Options map[string]string Message string MessageIsErr bool PageState Pages BookMarks Bookmarks TopBar Headbar FootBar Footbar Certs gemini.TofuDigest } //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ func (c *client) GetSizeOnce() { var w, h = termios.GetWindowSize() c.Height = h c.Width = w } func (c *client) GetSize() { c.GetSizeOnce() c.SetMessage("Loading...", false) c.Draw() for { var w, h = termios.GetWindowSize() if h != c.Height || w != c.Width { c.Height = h c.Width = w c.SetPercentRead() c.Draw() } time.Sleep(500 * time.Millisecond) } } func (c *client) Draw() { var screen strings.Builder screen.Grow(c.Height*c.Width + c.Width) screen.WriteString("\033[0m") screen.WriteString(c.TopBar.Render(c.Width, c.Options["theme"])) screen.WriteString("\n") pageContent := c.PageState.Render(c.Height, c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) var re *regexp.Regexp if c.Options["theme"] == "inverse" { screen.WriteString("\033[7m") } re = regexp.MustCompile(`\033\[(?:\d*;?)+[A-Za-z]`) if c.BookMarks.IsOpen { bm := c.BookMarks.Render(c.Width, c.Height) bmWidth := len([]rune(bm[0])) for i := 0; i < c.Height-3; i++ { if c.Width > bmWidth { contentWidth := c.Width - bmWidth if i < len(pageContent) { extra := 0 if c.Options["theme"] == "color" { escapes := re.FindAllString(pageContent[i], -1) for _, esc := range escapes { extra += len(esc) } } screen.WriteString(fmt.Sprintf("%-*.*s", contentWidth+extra, contentWidth+extra, pageContent[i])) } else { screen.WriteString(fmt.Sprintf("%-*.*s", contentWidth, contentWidth, " ")) } screen.WriteString("\033[500C\033[39D") } if c.Options["theme"] == "inverse" && !c.BookMarks.IsFocused { screen.WriteString("\033[2;7m") } else if !c.BookMarks.IsFocused { screen.WriteString("\033[2m") } if c.Options["theme"] == "color" { screen.WriteString("\033[0m") } screen.WriteString(bm[i]) if c.Options["theme"] == "inverse" && !c.BookMarks.IsFocused { screen.WriteString("\033[7;22m") } else if !c.BookMarks.IsFocused { screen.WriteString("\033[0m") } screen.WriteString("\n") } } else { for i := 0; i < c.Height-3; i++ { if i < len(pageContent) { screen.WriteString("\033[0K") screen.WriteString(pageContent[i]) screen.WriteString("\n") } else { screen.WriteString("\033[0K") screen.WriteString("\n") } } } screen.WriteString("\033[0m") // TODO using message here breaks on resize, must regenerate screen.WriteString(c.RenderMessage()) screen.WriteString("\n") // for the input line screen.WriteString(c.FootBar.Render(c.Width, c.PageState.Position, c.Options["theme"])) // cui.Clear("screen") cui.MoveCursorTo(0, 0) fmt.Print(screen.String()) } func (c *client) TakeControlInput() { input := cui.Getch() switch input { case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0': // Quick link if input == '0' { c.goToLink("10") } else { c.goToLink(string(input)) } case 'j': // Scroll down one line c.ClearMessage() c.Scroll(1) case 'k': // Scroll up one line c.ClearMessage() c.Scroll(-1) case 'q': // Quit cui.Exit(0, "") case 'g': // Scroll to top c.ClearMessage() c.Scroll(-len(c.PageState.History[c.PageState.Position].WrappedContent)) case 'G': // Scroll to bottom c.ClearMessage() c.Scroll(len(c.PageState.History[c.PageState.Position].WrappedContent)) case 'd': // Scroll down 75% c.ClearMessage() distance := c.Height - c.Height/4 c.Scroll(distance) case 'u': // Scroll up 75% c.ClearMessage() distance := c.Height - c.Height/4 c.Scroll(-distance) case 'U': // Move up a directory for the current host url := c.PageState.History[c.PageState.Position].Location.Full c.Visit(UpOneDir(url)) case 'b', 'h': // Go back c.ClearMessage() err := c.PageState.NavigateHistory(-1) if err != nil { c.SetMessage(err.Error(), false) c.DrawMessage() } else { c.SetHeaderUrl() c.SetPercentRead() c.Draw() } case 'R': // Refresh the current page c.ClearMessage() err := c.ReloadPage() if err != nil { c.SetMessage(err.Error(), false) c.DrawMessage() } else { c.Draw() } case 'B': // Toggle the bookmark browser c.BookMarks.ToggleOpen() c.Draw() case 'f', 'l': // Go forward c.ClearMessage() err := c.PageState.NavigateHistory(1) if err != nil { c.SetMessage(err.Error(), false) c.DrawMessage() } else { c.SetHeaderUrl() c.SetPercentRead() c.Draw() } case '\t': // Toggle bookmark browser focus c.BookMarks.ToggleFocused() c.Draw() case 'n': // Next search item c.ClearMessage() err := c.NextSearchItem(1) if err != nil { c.SetMessage(err.Error(), false) c.DrawMessage() } case 'N': // Previous search item c.ClearMessage() err := c.NextSearchItem(-1) if err != nil { c.SetMessage(err.Error(), false) c.DrawMessage() } case '/': // Search for text c.ClearMessage() c.ClearMessageLine() if c.Options["theme"] == "normal" || c.Options["theme"] == "color" { fmt.Printf("\033[7m%*.*s\r", c.Width, c.Width, "") } entry, err := cui.GetLine("/") c.ClearMessageLine() if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() break } err = c.find(entry) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() } err = c.NextSearchItem(0) if err != nil { c.PageState.History[c.PageState.Position].WrapContent(c.Width-1,getMaxWidth(c.Options),(c.Options["theme"] == "color")) c.Draw() } case ':', ' ': // Process a command c.ClearMessage() c.ClearMessageLine() if c.Options["theme"] == "normal" || c.Options["theme"] == "color" { fmt.Printf("\033[7m%*.*s\r", c.Width, c.Width, "") } entry, err := cui.GetLine(": ") c.ClearMessageLine() if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() break } else if strings.TrimSpace(entry) == "" { c.DrawMessage() break } parser := cmdparse.NewParser(strings.NewReader(entry)) p, err := parser.Parse() if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() } else { err := c.routeCommandInput(p) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() } } } } func (c *client) routeCommandInput(com *cmdparse.Command) error { switch com.Type { case cmdparse.SIMPLE: c.simpleCommand(com.Action) case cmdparse.GOURL: c.goToURL(com.Target) case cmdparse.GOLINK: c.goToLink(com.Target) case cmdparse.DO: c.doCommand(com.Action, com.Value) case cmdparse.DOLINK: c.doLinkCommand(com.Action, com.Target) case cmdparse.DOAS: c.doCommandAs(com.Action, com.Value) case cmdparse.DOLINKAS: c.doLinkCommandAs(com.Action, com.Target, com.Value) default: return fmt.Errorf("Unknown command entry") } return nil } func (c *client) simpleCommand(action string) { action = strings.ToUpper(action) switch action { case "Q", "QUIT": cui.Exit(0, "") case "H", "HOME": if c.Options["homeurl"] != "unset" { go c.Visit(c.Options["homeurl"]) } else { c.SetMessage(fmt.Sprintf("No home address has been set"), false) c.DrawMessage() } case "B", "BOOKMARKS": c.BookMarks.ToggleOpen() c.Draw() case "R", "RELOAD": c.ClearMessage() err := c.ReloadPage() if err != nil { c.SetMessage(err.Error(), false) c.DrawMessage() } else { c.Draw() } case "SEARCH": c.search("", "", "?") case "HELP", "?": c.Visit(helplocation) case "JUMP", "J": err := c.PageState.CopyHistory(-1) if err != nil { c.SetMessage(err.Error(), false) c.DrawMessage() } else { c.Draw() } case "VERSION": ver := version if ver == "" { ver = "Improperly compiled, no version information" } c.SetMessage("Bombadillo version: " + ver, false) c.DrawMessage() default: c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } func (c *client) doCommand(action string, values []string) { switch action { case "C", "CHECK": c.displayConfigValue(values[0]) c.DrawMessage() case "HELP", "?": if val, ok := ERRS[values[0]]; ok { c.SetMessage("Usage: " + val, false) } else { msg := fmt.Sprintf("%q is not a valid command; help syntax: %s", values[0], ERRS[action]) c.SetMessage(msg, false) } c.DrawMessage() case "PURGE", "P": err := c.Certs.Purge(values[0]) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } if values[0] == "*" { c.SetMessage("All certificates have been purged", false) c.DrawMessage() } else { c.SetMessage(fmt.Sprintf("The certificate for %q has been purged", strings.ToLower(values[0])), false) c.DrawMessage() } err = saveConfig() if err != nil { c.SetMessage("Error saving purge to file", true) c.DrawMessage() } case "SEARCH": c.search(values[0], "", "") case "WRITE", "W": if values[0] == "." { values[0] = c.PageState.History[c.PageState.Position].Location.Full } u, err := MakeUrl(values[0]) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } fns := strings.Split(u.Resource, "/") var fn string if len(fns) > 0 { fn = strings.Trim(fns[len(fns)-1], "\t\r\n \a\f\v") } else { fn = "index" } if fn == "" { fn = "index" } c.saveFile(u, fn) default: c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } func (c *client) doCommandAs(action string, values []string) { switch action { case "ADD", "A": if len(values) < 2 { c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() return } if values[0] == "." { values[0] = c.PageState.History[c.PageState.Position].Location.Full } msg, err := c.BookMarks.Add(values) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } c.SetMessage(msg, false) c.DrawMessage() err = saveConfig() if err != nil { c.SetMessage("Error saving bookmark to file", true) c.DrawMessage() } if c.BookMarks.IsOpen { c.Draw() } case "SEARCH": if len(values) < 2 { c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() return } c.search(strings.Join(values, " "), "", "") case "SET", "S": if len(values) < 2 { c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() return } if _, ok := c.Options[values[0]]; ok { val := strings.Join(values[1:], " ") if !validateOpt(values[0], val) { c.SetMessage(fmt.Sprintf("Invalid setting for %q", values[0]), true) c.DrawMessage() return } c.Options[values[0]] = lowerCaseOpt(values[0], val) if values[0] == "geminiblocks" { gemini.BlockBehavior = c.Options[values[0]] } else if values[0] == "timeout" { updateTimeouts(c.Options[values[0]]) } else if values[0] == "configlocation" { c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true) c.DrawMessage() return } err := saveConfig() if err != nil { c.SetMessage("Value set, but error saving config to file", true) c.DrawMessage() } else { c.SetMessage(fmt.Sprintf("%s is now set to %q", values[0], c.Options[values[0]]), false) c.Draw() } return } c.SetMessage(fmt.Sprintf("Unable to set %s, it does not exist", values[0]), true) c.DrawMessage() default: c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } func (c *client) doLinkCommandAs(action, target string, values []string) { num, err := strconv.Atoi(target) if err != nil { c.SetMessage(fmt.Sprintf("Expected link number, got %q", target), true) c.DrawMessage() return } num-- links := c.PageState.History[c.PageState.Position].Links if num >= len(links) || num < 0 { c.SetMessage(fmt.Sprintf("Invalid link id: %s", target), true) c.DrawMessage() return } switch action { case "ADD", "A": bm := make([]string, 0, 5) bm = append(bm, links[num]) bm = append(bm, values...) msg, err := c.BookMarks.Add(bm) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } c.SetMessage(msg, false) c.DrawMessage() err = saveConfig() if err != nil { c.SetMessage("Error saving bookmark to file", true) c.DrawMessage() } if c.BookMarks.IsOpen { c.Draw() } case "WRITE", "W": out := make([]string, 0, len(values)+1) out = append(out, links[num]) out = append(out, values...) c.doCommandAs(action, out) default: c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } func (c *client) saveFile(u Url, name string) { var file []byte var err error c.SetMessage(fmt.Sprintf("Saving %s ...", name), false) c.DrawMessage() switch u.Scheme { case "gopher": file, err = gopher.Retrieve(u.Host, u.Port, u.Resource) case "gemini": file, err = gemini.Fetch(u.Host, u.Port, u.Resource, &c.Certs) case "http", "https": file, err = http.Fetch(u.Full) default: c.SetMessage(fmt.Sprintf("Saving files over %s is not supported", u.Scheme), true) c.DrawMessage() return } if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } // We are ignoring the error here since WriteFile will // generate the same error, and will handle the messaging savePath, _ := findAvailableFileName(c.Options["savelocation"], name) err = ioutil.WriteFile(savePath, file, 0644) if err != nil { c.SetMessage("Error writing file: "+err.Error(), true) c.DrawMessage() return } c.SetMessage(fmt.Sprintf("File saved to: %s", savePath), false) c.DrawMessage() } func (c *client) saveFileFromData(d, name string) { data := []byte(d) c.SetMessage(fmt.Sprintf("Saving %s ...", name), false) c.DrawMessage() // We are ignoring the error here since WriteFile will // generate the same error, and will handle the messaging savePath, _ := findAvailableFileName(c.Options["savelocation"], name) err := ioutil.WriteFile(savePath, data, 0644) if err != nil { c.SetMessage("Error writing file: "+err.Error(), true) c.DrawMessage() return } c.SetMessage(fmt.Sprintf("File saved to: %s", savePath), false) c.DrawMessage() } func (c *client) doLinkCommand(action, target string) { num, err := strconv.Atoi(target) if err != nil { c.SetMessage(fmt.Sprintf("Expected number, got %q", target), true) c.DrawMessage() } switch action { case "DELETE", "D": msg, err := c.BookMarks.Delete(num) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } c.SetMessage(msg, false) c.DrawMessage() err = saveConfig() if err != nil { c.SetMessage("Error saving bookmark deletion to file", true) c.DrawMessage() } if c.BookMarks.IsOpen { c.Draw() } case "BOOKMARKS", "B": if num > len(c.BookMarks.Links)-1 { c.SetMessage(fmt.Sprintf("There is no bookmark with ID %d", num), true) c.DrawMessage() return } c.Visit(c.BookMarks.Links[num]) case "CHECK", "C": num-- links := c.PageState.History[c.PageState.Position].Links if num >= len(links) || num < 0 { c.SetMessage(fmt.Sprintf("Invalid link id: %s", target), true) c.DrawMessage() return } link := links[num] c.SetMessage(fmt.Sprintf("[%d] %s", num+1, link), false) c.DrawMessage() case "JUMP", "J": num-- err = c.PageState.CopyHistory(num) if err != nil { c.SetMessage(err.Error(), false) c.DrawMessage() } else { c.Draw() } case "WRITE", "W": links := c.PageState.History[c.PageState.Position].Links if len(links) < num || num < 1 { c.SetMessage("Invalid link ID", true) c.DrawMessage() return } u, err := MakeUrl(links[num-1]) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } fns := strings.Split(u.Resource, "/") var fn string if len(fns) > 0 { fn = strings.Trim(fns[len(fns)-1], "\t\r\n \a\f\v") } else { fn = "index" } if fn == "" { fn = "index" } c.saveFile(u, fn) default: c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } func (c *client) search(query, uri, question string) { var entry string var err error if query == "" { c.ClearMessage() c.ClearMessageLine() if c.Options["theme"] == "normal" || c.Options["theme"] == "color" { fmt.Printf("\033[7m%*.*s\r", c.Width, c.Width, "") } fmt.Print(question) entry, err = cui.GetLine("? ") c.ClearMessageLine() if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } else if strings.TrimSpace(entry) == "" { c.ClearMessage() c.DrawMessage() return } } else { entry = query } if uri == "" { uri = c.Options["searchengine"] } u, err := MakeUrl(uri) if err != nil { c.SetMessage("The search url is not valid", true) c.DrawMessage() return } var rootUrl string switch u.Scheme { case "gopher": if ind := strings.Index(u.Full, "\t"); ind >= 0 { rootUrl = u.Full[:ind] } else { rootUrl = u.Full } c.Visit(fmt.Sprintf("%s\t%s", rootUrl, entry)) case "gemini": if ind := strings.Index(u.Full, "?"); ind >= 0 { rootUrl = u.Full[:ind] } else { rootUrl = u.Full } escapedEntry := url.PathEscape(entry) c.Visit(fmt.Sprintf("%s?%s", rootUrl, escapedEntry)) case "http", "https": c.Visit(u.Full) default: c.SetMessage(fmt.Sprintf("%q is not a supported protocol", u.Scheme), true) c.DrawMessage() } } func (c *client) Scroll(amount int) { if c.BookMarks.IsFocused { bottom := len(c.BookMarks.Titles) - c.Height + 5 // 3 for the three bars: top, msg, bottom if amount < 0 && c.BookMarks.Position == 0 { c.SetMessage("The bookmark ladder does not go up any further", false) c.DrawMessage() fmt.Print("\a") return } else if (amount > 0 && c.BookMarks.Position == bottom) || bottom < 0 { c.SetMessage("Feel the ground beneath your bookmarks", false) c.DrawMessage() fmt.Print("\a") return } newScrollPosition := c.BookMarks.Position + amount if newScrollPosition < 0 { newScrollPosition = 0 } else if newScrollPosition > bottom { newScrollPosition = bottom } c.BookMarks.Position = newScrollPosition } else { var percentRead int page := c.PageState.History[c.PageState.Position] bottom := len(page.WrappedContent) - c.Height + 3 // 3 for the three bars: top, msg, bottom if amount < 0 && page.ScrollPosition == 0 { c.SetMessage("You are already at the top", false) c.DrawMessage() fmt.Print("\a") return } else if (amount > 0 && page.ScrollPosition == bottom) || bottom < 0 { c.FootBar.SetPercentRead(100) c.SetMessage("You are already at the bottom", false) c.DrawMessage() fmt.Print("\a") return } newScrollPosition := page.ScrollPosition + amount if newScrollPosition < 0 { newScrollPosition = 0 } else if newScrollPosition > bottom { newScrollPosition = bottom } c.PageState.History[c.PageState.Position].ScrollPosition = newScrollPosition if len(page.WrappedContent) < c.Height-3 { percentRead = 100 } else { percentRead = int(float32(newScrollPosition+c.Height-3) / float32(len(page.WrappedContent)) * 100.0) } c.FootBar.SetPercentRead(percentRead) } c.Draw() } func (c *client) ReloadPage() error { if c.PageState.Length < 1 { return fmt.Errorf("There is no page to reload") } url := c.PageState.History[c.PageState.Position].Location.Full if c.PageState.Position == 0 { c.PageState.Position-- } else { err := c.PageState.NavigateHistory(-1) if err != nil { return err } } length := c.PageState.Length c.Visit(url) c.PageState.Length = length return nil } func (c *client) SetPercentRead() { page := c.PageState.History[c.PageState.Position] var percentRead int if len(page.WrappedContent) < c.Height-3 { percentRead = 100 } else { percentRead = int(float32(page.ScrollPosition+c.Height-3) / float32(len(page.WrappedContent)) * 100.0) } c.FootBar.SetPercentRead(percentRead) } func (c *client) displayConfigValue(setting string) { if val, ok := c.Options[setting]; ok { c.SetMessage(fmt.Sprintf("%s is set to: %q", setting, val), false) c.DrawMessage() } else { c.SetMessage(fmt.Sprintf("Invalid: %q does not exist", setting), true) c.DrawMessage() } } func (c *client) SetMessage(msg string, isError bool) { c.MessageIsErr = isError c.Message = strings.Replace(msg, "\t", "%09", -1) } func (c *client) DrawMessage() { cui.MoveCursorTo(c.Height-1, 0) fmt.Print(c.RenderMessage()) } func (c *client) RenderMessage() string { leadIn, leadOut := "", "" if c.Options["theme"] == "normal" || c.Options["theme"] == "color" { leadIn = "\033[7m" leadOut = "\033[0m" } if c.MessageIsErr { leadIn = "\033[31;1m" leadOut = "\033[0m" if c.Options["theme"] == "normal" || c.Options["theme"] == "color" { leadIn = "\033[41;1;7m" } } return fmt.Sprintf("%s%-*.*s%s", leadIn, c.Width, c.Width, c.Message, leadOut) } func (c *client) ClearMessage() { c.SetMessage("", false) } func (c *client) ClearMessageLine() { cui.MoveCursorTo(c.Height-1, 0) cui.Clear("line") } func (c *client) goToURL(u string) { if num, _ := regexp.MatchString(`^-?\d+.?\d*$`, u); num { c.goToLink(u) return } c.Visit(u) } func (c *client) goToLink(l string) { if num, _ := regexp.MatchString(`^-?\d+$`, l); num && c.PageState.Length > 0 { linkcount := len(c.PageState.History[c.PageState.Position].Links) item, err := strconv.Atoi(l) if err != nil { c.SetMessage(fmt.Sprintf("Invalid link id: %s", l), true) c.DrawMessage() return } if item <= linkcount && item > 0 { linkurl := c.PageState.History[c.PageState.Position].Links[item-1] c.Visit(linkurl) } else { c.SetMessage(fmt.Sprintf("Invalid link id: %s", l), true) c.DrawMessage() return } } } func (c *client) SetHeaderUrl() { if c.PageState.Length > 0 { u := c.PageState.History[c.PageState.Position].Location.Full c.TopBar.url = strings.Replace(u, "\t", "%09", -1) } else { c.TopBar.url = "" } } // Visit functions as a controller/router to the // appropriate protocol handler func (c *client) Visit(url string) { c.SetMessage("Loading...", false) c.DrawMessage() url = strings.Replace(url, "%09", "\t", -1) u, err := MakeUrl(url) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } switch u.Scheme { case "gopher": c.handleGopher(u) case "gemini": c.handleGemini(u) case "telnet": c.handleTelnet(u) case "http", "https": c.handleWeb(u) case "local": c.handleLocal(u) case "finger": c.handleFinger(u) default: c.SetMessage(fmt.Sprintf("%q is not a supported protocol", u.Scheme), true) c.DrawMessage() } } // +++ Begin Protocol Handlers +++ func (c *client) handleGopher(u Url) { if u.DownloadOnly || (c.Options["showimages"] == "false" && (u.Mime == "I" || u.Mime == "g")) { nameSplit := strings.Split(u.Resource, "/") filename := nameSplit[len(nameSplit)-1] filename = strings.Trim(filename, " \t\r\n\v\f\a") if filename == "" { filename = "gopherfile" } c.saveFile(u, filename) } else if u.Mime == "7" { c.search("", u.Full, "?") } else { content, links, err := gopher.Visit(u.Mime, u.Host, u.Port, u.Resource) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } pg := MakePage(u, content, links) if u.Mime == "I" || u.Mime == "g" { pg.FileType = "image" } else { pg.FileType = "text" } pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() c.SetHeaderUrl() c.Draw() } } func (c *client) handleGemini(u Url) { capsule, err := gemini.Visit(u.Host, u.Port, u.Resource, &c.Certs) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } go saveConfig() switch capsule.Status { case 1: // Query c.search("", u.Full, capsule.Content) case 2: // Success if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") { u.Mime = capsule.MimeMin pg := MakePage(u, capsule.Content, capsule.Links) pg.FileType = capsule.MimeMaj pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() c.SetHeaderUrl() c.Draw() } else { c.SetMessage("The file is non-text: writing to disk...", false) c.DrawMessage() nameSplit := strings.Split(u.Resource, "/") filename := nameSplit[len(nameSplit)-1] c.saveFileFromData(capsule.Content, filename) } case 3: // Redirect lowerRedirect := strings.ToLower(capsule.Content) lowerOriginal := strings.ToLower(u.Full) if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" { c.Visit(capsule.Content) } else { if !strings.Contains(capsule.Content, "://") { lnk, lnkErr := gemini.HandleRelativeUrl(capsule.Content, u.Full) if lnkErr == nil { capsule.Content = lnk } } c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false) c.DrawMessage() ch := cui.Getch() if ch == 'y' || ch == 'Y' { c.Visit(capsule.Content) } else { c.SetMessage("Redirect aborted", false) c.DrawMessage() } } } } func (c *client) handleTelnet(u Url) { c.SetMessage("Attempting to start telnet session", false) c.DrawMessage() msg, err := telnet.StartSession(u.Host, u.Port) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() } else { c.SetMessage(msg, true) c.DrawMessage() } c.Draw() } func (c *client) handleLocal(u Url) { content, links, err := local.Open(u.Resource) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } pg := MakePage(u, content, links) ext := strings.ToLower(filepath.Ext(u.Full)) if ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".png" { pg.FileType = "image" } pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() c.SetHeaderUrl() c.Draw() } func (c *client) handleFinger(u Url) { content, err := finger.Finger(u.Host, u.Port, u.Resource) if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } pg := MakePage(u, content, []string{}) pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() c.SetHeaderUrl() c.Draw() } func (c *client) handleWeb(u Url) { wm := strings.ToLower(c.Options["webmode"]) switch wm { case "lynx", "w3m", "elinks": if http.IsTextFile(u.Full) { page, err := http.Visit(wm, u.Full, c.Width-1) if err != nil { c.SetMessage(fmt.Sprintf("%s error: %s", wm, err.Error()), true) c.DrawMessage() return } pg := MakePage(u, page.Content, page.Links) pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() c.SetHeaderUrl() c.Draw() } else { c.SetMessage("The file is non-text: writing to disk...", false) c.DrawMessage() var fn string if i := strings.LastIndex(u.Full, "/"); i > 0 && i+1 < len(u.Full) { fn = u.Full[i+1:] } else { fn = "bombadillo.download" } c.saveFile(u, fn) } case "gui": c.SetMessage("Attempting to open in gui web browser", false) c.DrawMessage() msg, err := http.OpenInBrowser(u.Full) if err != nil { c.SetMessage(err.Error(), true) } else { c.SetMessage(msg, false) } c.DrawMessage() default: c.SetMessage("Current 'webmode' setting does not allow http/https", false) c.DrawMessage() } } func (c *client) find(s string) error { c.PageState.History[c.PageState.Position].SearchTerm = s c.PageState.History[c.PageState.Position].FindText() if s == "" { return nil } if len(c.PageState.History[c.PageState.Position].FoundLinkLines) == 0 { return fmt.Errorf("No text matching %q was found", s) } return nil } func (c *client) NextSearchItem(dir int) error { page := c.PageState.History[c.PageState.Position] if len(page.FoundLinkLines) == 0 { return fmt.Errorf("The search is over before it has begun") } c.PageState.History[c.PageState.Position].SearchIndex += dir page.SearchIndex += dir if page.SearchIndex < 0 { c.PageState.History[c.PageState.Position].SearchIndex = 0 page.SearchIndex = 0 } if page.SearchIndex >= len(page.FoundLinkLines) { c.PageState.History[c.PageState.Position].SearchIndex = len(page.FoundLinkLines) - 1 return fmt.Errorf("The search path goes no further") } else if page.SearchIndex < 0 { c.PageState.History[c.PageState.Position].SearchIndex = 0 return fmt.Errorf("You are at the beginning of the search path") } diff := page.FoundLinkLines[page.SearchIndex] - page.ScrollPosition c.ScrollForSearch(diff) c.Draw() return nil } func (c *client) ScrollForSearch(amount int) { var percentRead int page := c.PageState.History[c.PageState.Position] bottom := len(page.WrappedContent) - c.Height + 3 // 3 for the three bars: top, msg, bottom newScrollPosition := page.ScrollPosition + amount if newScrollPosition < 0 { newScrollPosition = 0 } else if newScrollPosition > bottom { newScrollPosition = bottom } c.PageState.History[c.PageState.Position].ScrollPosition = newScrollPosition if len(page.WrappedContent) < c.Height-3 { percentRead = 100 } else { percentRead = int(float32(newScrollPosition+c.Height-3) / float32(len(page.WrappedContent)) * 100.0) } c.FootBar.SetPercentRead(percentRead) c.Draw() } //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ // MakeClient returns a client struct and names the client after // the string that is passed in func MakeClient(name string) *client { c := client{0, 0, defaultOptions, "", false, MakePages(), MakeBookmarks(), MakeHeadbar(name), MakeFootbar(), gemini.MakeTofuDigest()} return &c } func findAvailableFileName(fpath, fname string) (string, error) { savePath := filepath.Join(fpath, fname) _, fileErr := os.Stat(savePath) for suffix := 1; fileErr == nil; suffix++ { fn := fmt.Sprintf("%s.%d", fname, suffix) savePath = filepath.Join(fpath, fn) _, fileErr = os.Stat(savePath) if !os.IsNotExist(fileErr) && fileErr != nil { return savePath, fileErr } } return savePath, nil } func syntaxErrorMessage(action string) string { if val, ok := ERRS[action]; ok { return fmt.Sprintf("Incorrect syntax. Try: %s", val) } return fmt.Sprintf("Unknown command %q", action) } func updateTimeouts(timeoutString string) error { sec, err := strconv.Atoi(timeoutString) if err != nil { return err } timeout := time.Duration(sec) * time.Second gopher.Timeout = timeout gemini.TlsTimeout = timeout return nil } // getMaxWidth looks through the given options map and will safely return a max width to render // if the option is missing or malformed, it will default to 100. A sane minimum of 10 is enforced. func getMaxWidth(options map[string]string) int { out, err := strconv.Atoi(options["maxwidth"]) if err != nil { out = 100 } if out < 10 { out = 10 } return out }bombadillo/bookmarks.go0000644000175000017500000000762214211226262015034 0ustar nileshnileshpackage main import ( "fmt" "strings" "tildegit.org/sloum/bombadillo/cui" ) //------------------------------------------------\\ // + + + T Y P E S + + + \\ //--------------------------------------------------\\ // Bookmarks represents the contents of the bookmarks // bar, as well as its visibility, focus, and scroll // state. type Bookmarks struct { IsOpen bool IsFocused bool Position int Length int Titles []string Links []string } //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ // Add a bookmark to the bookmarks struct func (b *Bookmarks) Add(v []string) (string, error) { if len(v) < 2 { return "", fmt.Errorf("Received %d arguments, expected 2+", len(v)) } b.Titles = append(b.Titles, strings.Join(v[1:], " ")) b.Links = append(b.Links, v[0]) b.Length = len(b.Titles) return "Bookmark added successfully", nil } // Delete a bookmark from the bookmarks struct func (b *Bookmarks) Delete(i int) (string, error) { if i < len(b.Titles) && len(b.Titles) == len(b.Links) { b.Titles = append(b.Titles[:i], b.Titles[i+1:]...) b.Links = append(b.Links[:i], b.Links[i+1:]...) b.Length = len(b.Titles) return "Bookmark deleted successfully", nil } return "", fmt.Errorf("Bookmark %d does not exist", i) } // ToggleOpen toggles visibility state of the bookmarks bar func (b *Bookmarks) ToggleOpen() { b.IsOpen = !b.IsOpen if b.IsOpen { b.IsFocused = true } else { b.IsFocused = false } } // ToggleFocused toggles the focal state of the bookmarks bar func (b *Bookmarks) ToggleFocused() { if b.IsOpen { b.IsFocused = !b.IsFocused } } // IniDump returns a string representing the current bookmarks // in the format that .bombadillo.ini uses func (b Bookmarks) IniDump() string { if len(b.Titles) < 1 { 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 } // List returns a list, including link nums, of bookmarks // as a string slice 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 } // Render returns a string slice with the contents of each // visual row of the bookmark bar. func (b Bookmarks) Render(termwidth, termheight int) []string { width := 40 termheight -= 3 var walll, wallr, floor, ceil, tr, tl, br, bl string if termwidth < 40 { width = termwidth } if b.IsFocused { walll = cui.Shapes["awalll"] wallr = cui.Shapes["awallr"] ceil = cui.Shapes["aceiling"] floor = cui.Shapes["afloor"] tr = cui.Shapes["atr"] br = cui.Shapes["abr"] tl = cui.Shapes["atl"] bl = cui.Shapes["abl"] } else { walll = cui.Shapes["walll"] wallr = cui.Shapes["wallr"] ceil = cui.Shapes["ceiling"] floor = cui.Shapes["floor"] tr = cui.Shapes["tr"] br = cui.Shapes["br"] tl = cui.Shapes["tl"] bl = cui.Shapes["bl"] } out := make([]string, 0, 5) contentWidth := width - 2 top := fmt.Sprintf("%s%s%s", tl, strings.Repeat(ceil, contentWidth), tr) out = append(out, top) marks := b.List() for i := 0; i < termheight-2; i++ { if i+b.Position >= len(b.Titles) { out = append(out, fmt.Sprintf("%s%-*.*s%s", walll, contentWidth, contentWidth, "", wallr)) } else { out = append(out, fmt.Sprintf("%s%-*.*s%s", walll, contentWidth, contentWidth, marks[i+b.Position], wallr)) } } bottom := fmt.Sprintf("%s%s%s", bl, strings.Repeat(floor, contentWidth), br) out = append(out, bottom) return out } //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ // MakeBookmarks creates a Bookmark struct with default values func MakeBookmarks() Bookmarks { return Bookmarks{false, false, 0, 0, make([]string, 0), make([]string, 0)} } bombadillo/bombadillo.desktop0000644000175000017500000000052014211226262016202 0ustar nileshnilesh[Desktop Entry] Type=Application Name=Bombadillo GenericName=Non-Web Browser Comment=View gopher, gemini, finger, telnet, http(s) sites over the internet Terminal=true Categories=Network;WebBrowser;ConsoleOnly; Exec=bombadillo -t %u Icon=bombadillo-icon MimeType=x-scheme-handler/gopher;x-scheme-handler/gemini;x-scheme-handler/finger; bombadillo/bombadillo.10000644000175000017500000003302614211226262014700 0ustar nileshnilesh.TH "bombadillo" 1 "27 OCT 2019" "" "General Operation Manual" .SH NAME \fBbombadillo \fP- a non-web browser .SH SYNOPSIS .nf .fam C \fBbombadillo\fP [\fIoptions\fP] [\fIurl\fP] .fam T .fi .SH DESCRIPTION \fBbombadillo\fP is a non-web browser for the terminal. It features a full terminal user interface, vim-like keybindings, document pager, configurable settings, and a robust command selection. .TP \fBbombadillo\fP supports the following protocols as first class citizens: gopher, gemini, finger, and local (a user’s file system). Support for telnet, http and https is also available via integration with third party applications. .SH OPTIONS .TP .B \fB-h\fP Display usage help and exit. Provides a list of all command line options with a short description and exits. .TP .B \fB-t\fP Set the window title to 'Bombadillo'. Can be used in a GUI environment, however not all terminals support this feature. .TP .B \fB-v\fP Display version information and exit. .SH PROTOCOL SUPPORT All of the below protocols are supported. With the exception of gopher, the protocol name must be present as the scheme component of a url in the form of \fI[protocol]://[the rest of the url]\fP. .TP .B gopher Gopher is the default protocol for \fBbombadillo\fP. Any textual item types will be visited and shown to the user and any non-text types will be downloaded. Type 7 (querying) is fully supported. As the default protocol, any url that is not prefixed with the scheme section of a url (\fIgopher://\fP for example) will be treated as gopher urls. .TP .B gemini Gemini is supported, but as a new protocol with an incomplete specification, features may change over time. At present Bombadillo supports TLS with a trust on first use certificate pinning system (similar to SSH). Gemini maps and other text types are rendered in the browser and non-text types will be downloaded. .TP .B finger Basic support is provided for the finger protocol. The format is: \fIfinger://[[username@]][hostname]\fP. Many servers still support finger and it can be fun to see if friends are online or read about the users whose phlogs you follow. .TP .B local Local is similar to the \fIfile\fP protocol used in web browsers or the like, with a smaller set of features. Users can use the local scheme to view files on their local system. Directories are supported as viewable text object as well as any files. Wildcards and globbing are not supported. Using \fI~\fP to represent a user's home directory, as well as relative paths, are supported. The \fIcolor\fP theme has no effect on this protocol and all terminal escape sequences will be rendered to the screen literally. .TP .B telnet Telnet is not supported directly, but addresses will be followed and opened as a subprocess by whatever telnet client a user sets in their settings (defaulting to \fItelnet\fP). In some cases this behavior may be buggy. .TP .B http, https Neither of the world wide web protocols are supported directly. \fBbombadillo\fP can be configured to open web links in a user's default graphical web browser. It is also possible to display web content directly in \fBbombadillo\fP using lynx, w3m, or elinks terminal web browsers to render pages. Opening http/https links is opt-in only, controlled by the \fIwebmode\fP setting. .IP Opening links in a default graphical web browser will only work in a GUI environment. .IP Displaying web content directly in \fBbombadillo\fP requires lynx, w3m or elinks terminal web browsers are installed on the system. .SH COMMANDS .SS KEY COMMANDS These commands work as a single keypress anytime \fBbombadillo\fP is not taking in a line based command or when the user is being prompted for action. This is the default command mode of \fBbombadillo\fP. .TP .B b, h Navigate back one place in your document history. .TP .B B Toggle the bookmarks panel open/closed. .TP .B d Scroll down an amount corresponding to 75% of your terminal window height in the current document. .TP .B f, l Navigate forward one place in your document history. .TP .B g Scroll to the top of the current document. .TP .B G Scroll to the bottom of the current document. .TP .B j Scroll down a single line in the current document. .TP .B k Scroll up a single line. .TP .B n Jump to next found text item. .TP .B N Jump to previous found text item. .TP .B q Quit \fBbombadillo\fP. .TP .B R Reload the current page (does not destroy forward history). .TP .B 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 Quick navigation to the first 10 links on a page. The 0 key will navigate to the link numbered '10', all other numbers navigate to their matching link number. .TP .B U Move up a level in the current url path. \fI/mydir/mysubdir/myfile.txt\fP would become \fI/mydir/mysubdir/\fP, and so on. .TP .B u Scroll up an amount corresponding to 75% of your terminal window height in the current document. .TP .B / Search for text within current document. / followed by a text query will highlight and allow navigation of found text. / with an empty query will clear the current query. .TP .B Toggle the scroll focus between the bookmarks panel and the document panel. Only has an effect if the bookmarks panel is open. .TP .B Enter line command mode. Once a line command is input, the mode will automatically revert to key command mode. .TP .B : Alias for . Enter line command mode. .SS LINE COMMANDS These commands are typed in by the user to perform an action of some sort. As listed in KEY COMMANDS, this mode is initiated by pressing : or . The command names themselves are not case sensitive, though the arguments supplied to them may be. .TP .B [url] Navigates to the requested url. .TP .B [link id] Follows a link on the current document with the given number. .TP .B add [url] [name\.\.\.] Adds the url as a bookmarks labeled by name. \fIa\fP can be used instead of the full \fIadd\fP. .TP .B add [link id] [name\.\.\.] Adds the url represented by the link id within the current document as a bookmark labeled by name. \fIa\fP can be used instead of the full \fIadd\fP. .TP .B add . [name\.\.\.] Adds the current document's url as a bookmark labeled by name. \fIa\fP can be used instead of the full \fIadd\fP. .TP .B bookmarks Toggles the bookmarks panel open/closed. Alias for KEY COMMAND \fIB\fP. \fIb\fP can be used instead of the full \fIbookmarks\fP. .TP .B bookmarks [bookmark id] Navigates to the url represented by the bookmark matching bookmark id. \fIb\fP can be entered, rather than the full \fIbookmarks\fP. .TP .B check [link id] Displays the url corresponding to a given link id for the current document. \fIc\fP can be used instead of the full \fIcheck\fP. .TP .B check [setting name] Displays the current value for a given configuration setting. \fIc\fP can be used instead of the full \fIcheck\fP. .TP .B delete [bookmark id] Deletes the bookmark matching the bookmark id. \fId\fP can be used instead of the full \fIdelete\fP. .TP .B help Navigates to the gopher based help page for \fBbombadillo\fP. \fI?\fP can be used instead of the full \fIhelp\fP. .TP .B home Navigates to the document set by the \fIhomeurl\fP setting. \fIh\fP can be entered, rather than the full \fIhome\fP. .TP .B jump Navigates to the previous page in history from the current page. Useful for keeping the current page in your history while still browsing. \fIj\fP can be used instead of the full \fIjump\fP. .TP .B jump [history location] Navigates to the given history location. The history location should be an integer between 0 and 20. \fIj\fP can be used instead of the full \fIjump\fP. .TP .B purge * Deletes all pinned gemini server certificates. \fIp\fP can be used instead of the full \fIpurge\fP. .TP .B purge [host name] Deletes the pinned gemini server certificate for the given hostname. \fIp\fP can be used instead of the full \fIpurge\fP. .TP .B quit Quits \fBbombadillo\fP. Alias for KEY COMMAND \fIq\fP. \fIq\fP can be used instead of the full \fIquit\fP. .TP .B reload Requests the current document from the server again. This does not break forward history the way entering the url again would. \fIr\fP can be used instead of the full \fIreload\fP. .TP .B search Queries the user for search terms and submits a search to the search engine set by the \fIsearchengine\fP setting. .TP .B search [keywords\.\.\.] Submits a search to the search engine set by the \fIsearchengine\fP setting, with the query being the provided keyword(s). .TP .B set [setting name] [value] Sets the value for a given configuration setting. \fIs\fP can be used instead of the full \fIset\fP. .TP .B version Shows the current Bombadillo version number. .TP .B write . Writes the current document to a file. The file is named by the last component of the url path. If the last component is blank or \fI/\fP a default name will be used. The file saves to the directory set by the \fIsavelocation\fP setting. \fIw\fP can be entered rather than the full \fIwrite\fP. .TP .B write [url] Writes data from a given url to a file. The file is named by the last component of the url path. If the last component is blank or \fI/\fP a default name will be used. The file saves to the directory set by the \fIsavelocation\fP setting. \fIw\fP can be entered rather than the full \fIwrite\fP. .TP .B write [link id] Writes data from a given link id in the current document to a file. The file is named by the last component of the url path. If the last component is blank or \fI/\fP a default name will be used. The file saves to the directory set by the \fIsavelocation\fP setting. \fIw\fP can be entered rather than the full \fIwrite\fP. .SH FILES \fBbombadillo\fP keeps a hidden configuration file in a user's XDG configuration directory. The file is a simplified ini file titled \fI.bombadillo.ini\fP. It is generated when a user first loads \fBbombadillo\fP and is updated with bookmarks and settings as a user adds them. The file can be directly edited, but it is best to use the SET command to update settings whenever possible. To return to the state of a fresh install, simply remove the file and a new one will be generated with the \fBbombadillo\fP defaults. On some systems an administrator may set the configuration file location to somewhere other than the default setting. If you do not see the file where you expect it, or if your settings are not being read, try \fI:check configlocation\fP to see where the file should be, or contact your system administrator for more information. .SH SETTINGS The following is a list of the settings that \fBbombadillo\fP recognizes, as well as a description of their valid values. .TP .B configlocation The path to the directory that the \fI.bombadillo.ini\fP configuration file is stored in. This is a \fBread only\fP setting and cannot be changed with the \fIset\fP command, but it can be read with the \fIcheck\fP command. .TP .B defaultscheme The scheme that should be used when no scheme is present in a given URL. \fIgopher\fP, \fIgemini\fP, \fIhttp\fP, and \fIhttps\fP are valid values. .TP .B geminiblocks Determines how to treat preformatted text blocks in text/gemini documents. \fIblock\fP will show the contents of the block, \fIalt\fP will show any available alt text for the block, \fIboth\fP will show both the content and the alt text, and \fIneither\fP will show neither. Unlike other settings, a change to this value will require a fresh page load to see the change. .TP .B homeurl The url that \fBbombadillo\fP navigates to when the program loads or when the \fIhome\fP or \fIh\fP LINE COMMAND is issued. This should be a valid url. If a scheme/protocol is not included, gopher will be assumed. .TP .B maxwidth The number of characters at which lines should be wrapped. If this is bigger than the available terminal width, the full width of the terminal will be used. If a non-integer or an integer less than 10 is given, a default value will be used. .TP .B savelocation The path to the directory that \fBbombadillo\fP should write files to. This must be a valid filepath for the system, must be a directory, and must already exist. .TP .B searchengine The url to use for the LINE COMMAND \fIsearch\fP. Should be a valid search path that terms may be appended to. .TP .B telnetcommand Tells the browser what command to use to start a telnet session. Should be a valid command, including any flags. The address being navigated to will be added to the end of the command. .TP .B theme Can toggle between visual modes. Valid values are \fInormal\fP, \fIcolor\fP, and \fIinverse\fP. When set to inverse, the normal mode colors are inverted. Both normal and inverse modes filter out terminal escape sequences. When set to color, Bombadillo will render terminal escape sequences representing colors when it finds them in documents. .TP .B timeout The number of seconds after which connections to gopher or gemini servers should time out if the server has not responded. .TP .B webmode Controls behavior when following web links. The following values are valid: \fInone\fP will disable following web links, \fIgui\fP will have the browser attempt to open web links in a user's default graphical web browser; \fIlynx\fP, \fIw3m\fP, and \fIelinks\fP will have the browser attempt to use the selected terminal web browser to handle the rendering of web pages and will display the pages directly in Bombadillo. .SH BUGS There are very likely bugs. Many known bugs can be found in the issues section of \fBbombadillo\fP's source code repository (see \fIlinks\fP). .SH LINKS \fBbombadillo\fP maintains a presence in the following locations: .TP .B Source Code Repository https://tildegit.org/sloum/bombadillo .TP .B Web Homepage http://bombadillo.colorfield.space .TP .B Gopher Homepage gopher://bombadillo.colorfield.space .SH AUTHORS \fBbombadillo\fP was primarily developed by sloum, with kind and patient assistance from ~asdf and jboverf. bombadillo/bombadillo-screenshot.png0000644000175000017500000014721614211226262017506 0ustar nileshnileshPNG  IHDR"An%zTXtRaw profile type exifxڵg\c=Z=76*E+u"uLm8ruůwj+ϯso5~v৾^u7[}M|M_o5 ^]]_9y}ҙss^p;?q}9Ε`lx)xRHΒ>qb|_;yܷo^nD>Η1;"+ [־{#Zݻ۽F.DM2+1wwds{C$77u%xbk+ZK5^R~+.5rH۵wηB;8og/tJ7z29ͧHHڋ_Jl"ܸ!k+<'>g>;6.,k5cq`wpܤTHN:7?S,2B",TI Dr6F K֬(bZQRkmRZi' z{='Ƙq2lϱ(jNeW{p(riqnv˭~Ƿ}}묅O֔>W/!Ĕ32s Ur[9*sʙHւ)9:#h7|˼9(oWsJFRy% S.vÞFO20Uo u=w}lǒrMLx8q9ޥ^h|v?sI,eZJ۝p+gk)muc,wH7{x\N9f١p+N j>7ͻm+TϨ3l^WeNnΏ|ʝ|''\u$Gf-o@\ktslRvi;Kq [tm Cγȍ gGD9w&2Kb&/o{δE Ua1KXjNqg7x smQ#P(=J sӷJkT|Tw\gBzZըz]jSMe]昧QHɩ.u}fsԌYj7lhV^[P7C5}X,iR҉ ︴P+Z1K+>.RwRUKoc68Og\NﴨWޖ듋z'(S^ "J8DZ@/cuz*H Zb+>&,vpݵfj)CM0D*h5@UӮVhLp(=WS0b Jd m-H1Ҵx,@">9iNM7As9z:/mqH&.TNG}pEΩqӫ԰GyEWo!A3}$ʉ"VDzf5[pN9]j}tB.> 0K}<&W}EOv(2H㺔RS Hr3xʢ]b])<(̙RPsWNK -@9)J; qYaw@nsO4>Awu*;V} cȕТW= .ܧUJRGkܟrPE~dj\ ċq 2Z|0)eֳ/ H rDj H&(=@ ]y餳 ?'Mxߧ̧"Z3ȧ'n (O N 1fT\!D&I| Z@vL  S%- G%:1o$~tDo -З^"P.X^A q*293!IZͦ;#|JuBZj*lI9VWJ CpQh?6.-OJ#ckh@*$I44/T=Qm޷$Lp7L+zYi%3Gp C>Sգ4Pr"ֆ4sXHĥ<`=JIn]$ݹ-F*RE!q7,dv&qkDuɥ(DͿ:{v ,ҶAhwZ\0lv=`l({ ޥi:/ȮBd?Ә_?LH >O +J^v'&A]3j܂4RЮx`*&3hchlqGuP'+P@HPPGъH'|O$}5@eP`A2'H*K rШVW,JdJY6 d$+Om1"pN 퓠L>)@d\Rؤqˁ ?hl/oht$ɗͭV(ک twH ʝ!0X' ?=6}t#EDiҢcar\@A;@5hQ!;Q NsƁ0p5tA@cs VMZ~2ncZD#nTriI`wrp\VGvsm ΀-kJ9$R@j@y}K<"{^2xh7H&̅>z*>L*qgp$|m#"e JYX)"\,yxBjU? ]<#)-=!q(|K`נ'.ܹY'D |=!hԬ(c΍]|sQYFwDE9/7蕢/BR.x>91jYUu$p9$U[d TT%#dD{*Bў5ضqv⯖dȸ21*wj|-8*vp)a_SЀv,5MȐN|5ύ%; !LD}p?*q$_0%$yP+M'TϠcZMLkl/1a.jQM([#DaNǯȻv?m]0G=y!8R"GˢOEͫX] %k:)gX'jm] h$k ;qF; i/cdlS)tHr0*-k!pTWHs@p&WZީ87UEX x~ t#bЈ=(87(]07.y q ⥻7xh&Z47',c9D&[qp1]w`9bž4] Cr# @Ł@4&)mt`fIw)Hz&3h$Ce UVy$pC2B1c_ nyq)C6uds-5 r߄q7A{Np*f5gP;7q޻Î^eOdӥ~-*!Ԍ6.w?͆]8 lI/G{Fڂ,JG~;s\: iY CFU T }:\N˜^ >,EFQ;6 Kn'6bFc}9ˈ)Yw O u0!ipԨ. &noq,5Z1x> a0&CT͵"%K|A4 J b,J!KPG^Ǝ}^A_jv]E/~jU# p7e[A೮&rH^A>ఛR(B|TBcV4.KX9 BffG+:@h #(u>cih j=Sbh89Լ~z4wK̃'јTeZ4b@Zp^p`|TA(7}(G亖s" D }[\Q$0L8"VߘFՁ1v AqR5'K:MsO B{ skqΉ8ω^@> +:Jrt;=8LVѺ > iRGrsx2%j0r|_s+iu03K')ϗ N"՚`'GKUjE+i ,r1#QKT&v%z'X܃t P5yq:Q3m_k8c2(;eqx*,-7>=`3QpC9gD&K[2We\| ўH[C^Ad™@etF|64}Ѷ|]U" 9QT/9oxp z9kje,!-c: 6~퀩;D<\>)ÿ\QezXj9j6mPacퟢQ.hNW$jG\qZUXj,i4P'ȆrmR`G%з]z"P^^ngDKZOۃ={-UC@VO/>KcKF;KZJ|mӫגoZS08ހ)WOfAGf%.oWa!>IN-3ۧ4RyR˭xYenT)͸NaX9WM4dhC1t$Q8$ 4o8rG(vD9zέ["PKx]ɕ7ݵގ@A4TzN( zGZ[d%aYA@mIrY{f@X7Q'&n #E886>KRua :A G"fEnICd&r!\Zؽ%跣Cj74p[ZuY\\BM|\k#ͳ(($/3^.L0?D'l8w룤ZvSEmnXuj[?o#ӐN &Ln ,!Mu/J4ȴjdqj":=?y[mzF,;kH}+MQB,@rQ۶8\[ѝ@aY>u?&/aJO-2q]r9i"iwQ!m8r 5+nZzކ\v&xK(pehz^IԊ`>7V~,RЇ{;@Q ${]ϵHA|zK.8EFclFYv^!4Q" tEPMUO3,4жbQW+pw>=,zneZ6 HR+rS# Hk[fJN|^O!k|/AUt-k<ОGTB?pT~PP޴£ XD9YAO44Ґ!Z=K{yמ@A+J!">M^݉W^T(3I &HA+3i\ x􎔌fgzAEzѻ,Fq4Bwzn :5FpZ:#cc_KAwR4p\e$$:4Q/ sÕ4ƛǠ*VQ5SPh)&H:9q98Ik+ [8$aC_~}lX ;DC?=]& ~?<Ā"1RW/ 'Z#:%El9qXѕtDo{ԶPLzzb^%ω{:/ex﷋rzEk~:.Vpi?^wC#ٿ)2b*1WEw"@8cp oǿwGz>k(㥍o k_g.X~OD%ސs)붰!0I0bxFm ᴸP\0y\=PNy*DF~' {t%{D=-&{Ug&›Y!I^rY[RS75);-W`` .M~=v!c;V@jh*L;@iݱrz-8-NJ+ξa+UY!?Y; Q ,U9f(0P#ڪۦ]?l%tKyOwJhi;luMq\_ɐ6:a;LhȮQ[[+F$v"bP` 8 X=v=sgFQSB!BofBHi!B&B!Bh!B!rqO/~|Əi ~%C{T432*wN&QfE^_LP1o)]<'3q^8)#^,Ks ?$f!cCȘcxI…:S2kDc|/P1&D;P|Р*C̛_~F}4IMaʢ!^J>b3Ix?^#ư7.ے0S b$:c.fhL̩&ejU㟗1Ri梑kEש_JuwSi|?1 wfB3Ox1T6ӎhV bԝ.Z=.Mʋ3ݩ9#mb健\Ę u dRǟ8>V,e_?ү[c ) S=<'||݈VS3eQӹ*$d-Ig70JEZ;$m[|˫2Õ[3xTUn)wM| )sG %kW/ +\yX+_|r8S'yύ,D }b -z̮Λ3R0,kNKۼALiU fezi/>a ^w'@Oq.͆y-WLq tn0h\fZn./^gMb_co)^[41'6ggmbBX:;6dUԉw0w ^2T^& Wpq"0k(Cҩv/\ywt<06mFƮPŨ{} ?®ܼ@FTUa;G'L= 1*t;$,6mG0iZkZLZĚK8}zP,1< {ֳW#?ry㷡 LKQcLNSӱ ֥]^ }kIȔt,Sh% ],FwCwC wh|v|5+Qө2jaiMQՀ.hXFFl]l8u|+QeUf_H[.aQ&le9-lOv酪h/d@Zta_gx3oCM"֨C]J4֝q_}*/Mێ*Kwg jէa2TsH*aYiG8+m*;yo>թ*W{W1 cZG"婑" 2TYzcfzsteM<,K.J_1w򊱡z:~ʼPEkӪD=ںx֧m E*U;(A̪ԧXnh=FvVY4ƺ,_r:qڢܯk[8^{)~},=߅Me>ڳ~ez}~kg`~οr1w *Nye:!uƍbroM~Y-/bFW5Y't}V-&xTP ?2K<>) o"Of Y3[7J}=+ﺡ+S z|ioΎJݘG܈\[ ߠ:3}@bB T_ۣ|gj~:ǘc>g k=_v<8S$ϩ0w {~kn*[ol})ː9}>I3tPPZ-:(F Pu(Je|}Z75ۉXZ)aix,z੺MbWL~tXf|gi~QN&3 >qSE1ԨW9\GnXVDp.僴kIrv,:Ds"QO[:wP=.:[p$.ajXAbQVQ4h*i^:-T@VӫBsºyOuAk=QQWw8)LELbl5,Wr욊_7ʸe8kCÍ Kj;oTi'h⺡x֫߳>{ Ot5lŹ9`myuMKmY-5|BUu]3MϘWڔ:xgݒOVP% 'сkg9{"#w1Wsf[F drUAѢ+WB:#) *¼Qw*s]]fmfyca>zl<ϸo 4j4afxHz'&[!TfjThp.{d@a)D"ִxZLK6TP(d,UVukLj{ۣÜӞ((3K_w'Mv_kD (Je~NV+Xn u3[VmdAߘ[@5'~^Y~7ڢ8fi@Y NDLDrP1@˝( I&kcJJФ߬ چ*įṃ|Izgڔ:Ϙc~D}3QQLov3TeoTVFx#*UH9ɹ>%eHzeNEI8ܛU-9t:V(N#]]N%FO_}£:p.K!9Y6XFLVGro||t< T)qfY X\p:;a !Pyj^. ""Qث%S;|O÷38AV~TOLxa^YoCvk6vY/SD~gtmePV9 [*^h >=Jq<0B $FL'>U-O"qKcl:J}yeil1̝ /ź}_qd.?-Y~ڢyOsU6 $(PJZ"B{ҾB´ժ͇k g%/L˰;TWjR^e+tmyrd> >~2[9iwkE]csqg"\o̡;сF-ʬg2:x/fud%EmbTҗNe/wDEq/>>QڗP睦 2{l(a$WUOوK(W숶֥zg. 'o 8rGN5]oh*&)KG-3jCj[ɱ(L kE<i쟿[d ڭ瓶VŬ:jxsml~=L?)}Aڒgz/ͼq2H8wN,[ >X%yf_/oJ.=ʡųYz$Lm'2-NJ P5ڸJ\ u_Dߩ}OEwZ^>?-'KW5eW=e YY:F9ڵ/F1OZT&CiZ@wk{Y,Pu6Nt&}yeeld:6s!VuQ(I(lfW0uCWg}X7?lJ/'f>P_L`Di5ށc&L=FvJQ\[pB IK֡¹_ V1(S3C3ab~+QniR|}~k~yWxdǘc[ .%d8zξo`W]c}cVv{Yp?[Ƹ1&EŜ$~K dm E} E3~E+p.WC>@aOѬh9o 5G/3&,Җ^5T}Ϭ$՛zoLgn-j*(qfT$(R1}Wz6Yn̓(=Rf7bɨq(\:Sm~.8~)䳧w Jۼ_*|~J.coU&3* NY=,])u,~zd}2S%;LgI%̟ê@ vN8d,CpeY;c#=Dt I"%1t:?vIsL Ƴ>ysg-@oJ\;Nᧄ2sN|j+hN.aP9…O%./bOWGIn|{,?-I((]_'6Wx}~kgvd>;֞:c,BndRIBz~ȿRbD,@_LiؓU5ƋyCtt_^&ESfp+pS!N!k,6t';cuMo1қ5\+F, wc*FڕF33dVw?&1)I{!kpa◾plLy+ch7-G!0ǑkSbO5Ewh'\?iKs.{CŘy,X7( ^ӏk9-C?iy!1֟<\#=!B!D%OG!B!IB!D !B!IB!D !B!IB!Bh!B!$B!Bh!B!$B!Bh!B!$B!BH-B!$B!BH-B!$B!BH-B!$Z!BIB!$Z!BI0qVq@!!dl!$Z򻡌mam`f&?4_:rTݖ ܰ$Yz%}lEY8OU+ma~u*FH-0mZc4WԽ3ixme26ězH`b}hZVzP1BhshO~` c*/PVݫC1~8ɖ!P@vIoTy #$y5.GS{\}GF1% e@gZz'dlIf;t:NfLO{cW$;[xJ64T$Z~LGF1TuM(Kf2)<ǖ1+O. ?_j1$Wo#eUb$"1/o7b6r䑑cL(T^C^26EvsA@~,A!QQmcM,+ҧ #$yY4.ykH6rI> .NL !c#oe+mqû/yKEy xФp6ܤH+?ʚJ64T$Z%oی"׃ڟlS 6)x  !c#xYgS\ĭnl*;ʫiCCIEaQ V~ ǒc,q>*!dl LjJ.Bzn1+IzEI9ɹ#eaw0Vit6gh=P1BhWr4*r%1I]EgMmzlXϢ ٸd@wc#^뙌Gm_:꩛6ft$ART2dgYy6֣ #$yA>*KAV9DYp?[5<6Sr(߂֏%phdO?ǀX1ϰ裮Lor՜\k2v6eUb$"$ 5huL1rI΀ԛ?:;Qɐylhx,E},e@?knot;NUhQo 0#:l;BvȗʫiCCIE.gYן.^s"ËbLohb&~B=6=q|T/$2E2(^)y #$5oÙ1r ]kӉ)A؞YH™ ~X3|FW ] JY3#o3 gE4c;zT64T=>!r%.^%qUI/r#i*`lxq($B!BtB!BH-B!$ YIhO4gx&W/ elo}釧1rYșYqޞ474Əek\O$ IDATB,mZ!aiY҇ԏ6 Sk3sXBI0'Mzq?0C 7w2 =/$Ze^6nl H:׌Qmcfaj1㼥cG֖llBH-2҂%ocM$&EZQecykdž->&5 8/$ZeN(r=]2R\ĭnnaj1cG֖iSBH-鲨oZc\3RraeJ1cG֖lhBH- _jӨ5[{LJZP) e:1cGIA !$dMqDa+b՜\kga*10%wejkD aj,9Ƕh λР]5,}D}L826L}dme%$B.˺4pDhuk/OUk#KcjǑa#kɗejkD a~#˶H}/ά΍iV *0pGdŚQVY>v>vYF ;(*[{xxRH}7!}! !IB! yC!BIB!$Z!63 {XQ{>3 bQzbPƶ6zYƫOn>/rcOeBh!H 'hMyafUZиmv #e y˽ MpldgTdȍu>5JIR\ĭUÜmQzA\> U>mhjc#;';EnsvQBH-FX ].LHaQ V~ ǒ\> XmhRc#;'EnsvQBH-F$ARTJ-jӨ5[{#ɐy[sh}ЄFvIO6͋XSY$ZTԜ\kz񪑏7RAF.K(+mh*c#;T';EnsvQBH- 4GvޅaUf.\]##筓64ncy뜝}*kD uk/OU翶OK9e(yd M`ldgty^:gg%DZfBh8_.~ Xp8m㕵ݓV[aũwgTU0b0TY}Z:gg%D=u B$K\Jlu$ݾś҆oSHβ$Z!B#!"o˥ #$yh λР]59F!0 V5huL1r$ZQ(^)FB!Le]\"x 4FD|kf2Z2c7iB/PVݫC1~8i;Z\#ݓiYBvB!IR:nnKYl#G[P]d]UNG$P[ |KBuq7~ʀδ0n c0 ݾsf;t:NfLO{cgܕ'(a 5<]'3yO]WQ$B!HUũӢ4l$HK +[Ƹ7v$`Ӥˬ4M#lnw=ڔ{jTx)y eihk8=#k:%AWInEʓ |GyoZzp*{\ݕV g&,'SS #1{7π]*ǸNE[B!2<5~mg+< o>jLw㠄{PkAa, CSo$4{lS9=`;NQ?vyT\{:&;3./Z{qtI1f94@讳^CYo1?c$tqb!8ȐKQGb|ь+>nSWɸM(l72IB!2N`c#~B "~jR"ٺɎ_yj*TxjBK t ާa<;y)쎛BDGΐ &h\c}qi{ =鴲Φݗx֢}>Toi?:(VJ)km݄If(/IB!2MOVP% /*Ui=Z%RPbTh6|/1f~qSvN?z@['gRK vv Fvt={Sϓhi(Gӹih^'4WYۭ7ˏk[}ne!BdDq& jW-C>n|5u-RlmVdC{cX}A2y} W&rnrY7>>:o8F cD%j=7TYIj+JʱHνLʶ4-SBxQ%7֤fR!B$Zs[з_+:m'nmKIyq7SFkn gΎ\LAʇyu+od P.߷4N3ݝgV ̜Q>IrCLY+_Rپ?֛ց>ǹs;2gpJ¦Lœh2R(CVecs ۧӵAPUm_mN͙8Yۛ" X/`Q-ctϊ(%<(7i\!$he U32%NگVRGz%RRk)i]s&ij{q,^ YtݚOcDj_Ȍޤ MĆCYudmFS-n+za`5iQP5nFEG_I6tҴB!0$ZENx7y sg#, ΰoJT#ƩxTɯRލ]#.*#Nu(Sf e>  e@gZzaVsmұlmCLf57m*Sv¶ mf,}~7ԶKݻ{sBw+;"b\j87P8V惉Ә7-$7v3; '|U| KWr,d/2mѩzg f~Ϗ1'XЯ(J@Q#f=Ñ#lسuS5q k7܋]ǹ !B穇8uZOggDu6m{}Q@0+MӶh3|G6A_ۢ&)I J؛0\Y~ZrХS}"Q_ev[0e~ݛP{ XQa"fHfS 3qZ1pu`MjnL{j]Pӿk˩ɋ;ؚR:b_ /d{1;qk]k܅B!<6~t.Wy|}8nʑ(A 4*ׂ&%XN His\K'}{- vr1LaO*]׌{eܞۍ&m 9½q~TfAz/F͘7>¼NƵoBe˸qv?VmJR"߬o4u(yQY-3ƄW!;4TY:w!B7I`c#~B_ȟ{Hnc*hA= PRBwwriXn:ß욷)(Jtoq nqq:mf?ê)g8}"d~+gFٸ-&0 OC)km݄IՏf(/%:-:qjq@e}T`w܅B'I'+(d=RRR89V+D [[ʝz~XW%f`֬s<;nΩtGzLjNx#B+ޓqz~[V%U@}IՌ%asg@Uv/Ǚ(Z&7s)QIQU^!N5:[[,ِXa+>ޓ _v )zMΟ8H,q>xJYڂQy ~s 4mb|9JWJy١}{"Rrzr~ kO\|ѱJ@}aM_@W{񗏳{dooV9P:p Iܿ|S3}EG! ݉Wₕ6ۧtvy MQ]8.Z_$,0h- uxf~9u/[i~L]H 1؛4ɗ{(ա?GjQ[Y}>?8`јA}3%ڬtĹpGoBU{ُB!0$ZENx7y sg#HUA\ aND*F}Gl=SB_>nKYl#G[P]d]UNG$P[ |KBuq7~ʀδ0nk1qJBȚLG/JWPh;Id>@+Օx=0e~*ǜН>Ǝȿ׿Ol۶3y?kצU6sݡYv0cش})2+D!Ǟ$V7>Mq!B$ZU:-J`FBі.8%W0qs)# IDAToLI YYi-FغEb{H!)YP KٔuM(KfF~F[uw뻨r4%Qsn5%٪ %UVTý8{4ʘPi .NvuqLj0E{T0zWkV2eh8w7wq e|k[TP@ bٳbJ7IϰaA1+[o}g9"w_üB<\%gQ˼B!D.)xrGIB7J;{l)prZ:Vp="uX+;<01?,Y)感yvܔS>t;}ə6)xR F'"y";.csx VA!x7YR^P(@sz [)$K~G5ʌV=4fP8:f%ڭURThUKko5JTgQmQ#fb C@a+uCBxh{}9Xz˼B5CaPlq9͝9䍧abGIv_w7Wrf1o,hFer~s3T|T,Jb/\wرlN/t4CD|6 @۔*ӿK%34m7G6e+üZJKlsR/`v'C""""4DNq!:~TWװ2C ^^qXHZzf"Kqk9zK;Ww+S!` sOt 0fJeJŏ%ש;޹)ݸ=oy( gJl9VA9:c\ Q8 M`l]qXKFAܒYl<~J6M}=,>4Qnbu 1er |6?4DFh]8.!]Ȝm{>cQrU|pJͳўكLx{'0iQ{л)+}ct߃+lۜnzө0FeqoSck_ϲC=we: Gzat==RpD@^oG+QY GR7O 8vD',""""i<Ӵ)1~99Y)0kdMZlYql\?/bg:.!]Dž %lfVKl:1b0v3*vCÆ t>?uEs$8!X>یegEj~m-&?J̸@Ea_ܩ)=pb>F֚|B~!vNwbx:Wqꍚow%n?"7`M/*zpYGDDD1s^~0˾cs]"d,dNbۯ;ycUakGWӀdRfšh^9n7Xr&3@yalLͫ5/1e#x=t-C髑 $ <]1)g c$``JR3 :t[&B(˿ǒ$~kܝO~l\(G9ڳ>"O6p<)%rf>"""")DH1JrfV.-&ﲑ/G&7w;V]0ZjPtP,wo ToCѻ%Ka 5X fR;Dz[πWsn&_i̅yOs^H+blh)7ٱWGͷ\ U#?D[w{2Pєy)q//_ˎ[}!)]#"""B'z+燏aŒ̬7BH*h0SN5rlXܪ@¿|*}Ǿ]s}k/0p\:d讻x\+՟bWlU˽îgzFqq5a;dN(ó_Oи[ӑd5Tvv>y'V?IgȐtkqM_ e;-fٯY,I_!J }0^^Bmh:Tk^ &\J5 QrKb_K\3u|-#ic >yXK'RVP=W̓Sm`bte +ݚQ:\[ ϴɐss l'e{f8N;0gcM8f&|ѕM.bd~ɓͧKk^5EYi}=X|mPT,[wNsμOÊ n+//< 0BxcXDn?1'(ǩ[]3X1F`K$-{eG8JJv{38z0#_|Nїgl︖8Xp mJ%fe!9^xpqj>E)U6᣾ck'An~-D_ ñ\c$y++ ?M1ХZ F_0fd$K-Tq#g0|֍αb&r±93ka{-T]I|%@}pi#[C"NJ c:>""""+Dcl<=ʳ$d=3Br4;jÊ'ޙxziؽ;n͸c'rL%kMzĴ7 mOe#%ĴMuڿsߚA=]DWڷnEYhO`oV}yq4)X8xev}ݰ%]$6C3B4Λ ww[Gt¯rj>""""Qƣ-Lv6Ϥ^7 r0_^=a'3UO~St?J_ؕeZ>5sȘ?ӣ2>0VEDDD$ݸvJ """"-""""Bɏ|;Ϲ?UHvLj'yXTӛfԟDDD$ BGLgL+Yx敷p^WR7HbK"""!L#܍%͛բ37Yx~]˛oA+ز7e\h3?;|GUwj֠6+S̺q- 8,nՌ2hp0a`/BMw^{{^?a Gbx-q[ʴi&M-,4Ϛ[WDDD ut,Q?`s)~Ŷe003O6D%rc/X*Er(~v;Q0q ^}L5 lشx//{nŠߌg'$f6aܛopT%oLB%L==g\Z%ȈH fiڔ 9zW`f= ٷC/=1eTgpՃ/\յ a;[. zұ :$fꨫ3ywKkju݄&_fL$v-;zϓL8>)v"J*ҶT|Y2I:w'?^]ɏmӾye+=jǏ`\p6TԓW.ͥk~loVŢl;GAߗ'%N8 DVTXgyC9/Ye߱9. D,dNbۯ;ycUakGWGѼZ#ᑤxll/Q8}ALj#zMd}{S&z>3p˞#קH>S&3~WmסSNW58߫vÏ*MfcX0>J* i[:,"""O{v)Db13{Mm.@0ߙuz,3\ ZL`ye#_ϏL&8`0T,Q3MY2b?y.b$36С޵H3ڿ U_M"|=lM88  ZkS7!: _+WZ+4T6n h)7.ܨ~('͔'?rm̶~ 2C:kmmSyEDDix:HK(-m;1k$e`P~~V,{Y؛]z#V 1{</:JB2&2*IOk$2aG(n_5GKi{ @r6ec&o ot ּ //<Mx{\NHKv.Efqϡ>Zofk?rv#6M,\akxϮa`vuyDoPyIhٞHږ8N[S'zLVhd:fdFp( -NZb䍧abGIv+//<{2X1F`{$U10Ԭ'6j'P^=%l|xK~^ ˞DX{,1vOG%RO @rJ郇9zϑ8uAmєr>wogjp%mYDDDN7)m'ظ?+kX&ϒ3[]=twڼQi0FQJNcAS96eZ<]yhR{8N/bl4diB:Ѻq]B<< }zg& R4ȚLW8r6O6r ñE-b&6&2wb-R8qKf񸓭f?/Q{|̇?b߱D2-I̻FLԂeGo8ўZgyC46O΃iߣ<>J lVV<2 _f,A51-͂4y'9 SaC-ͳўكLK6ʮ t~6}Q蛴ry-3a@ÙmOd|[|=1_ȹ[Yv="""1x!k7ihnSV*7N2J[DDDD䱻nY~>xut6c{Ѵ)FOeoՓEDDD$082v%bٶgc 62辌E/9DDDDD~L*BBBBB(D(D(D(D(DBBBBBB(D(D(DexOib& SlԜr޷MummO/26dL!v&^ Q wDD_k;XҼ9_-:o\9 ϯkym?7h[+MP[v>ԞA)|mVuaZfm'XܪCWe+cixa&ǿq94Okl/ 2-H._?͆v&=B^Sڿ9c&{ʢx<75/?m^8b3bm})ZUL9틲DKaO,1 ݟ ~5q]$*03{jT)2Cۉsخ0 tXfuE)+RysLd,>v.ޙ]]qq\&)hWr3;˃@(E)q#&Os8 qT"G"g Htld=ĦLT97keAdS`& SfU0mF_~}.>E u+9\NHK+f[ɏ3.(u{XWw*\%?lpA IDATb=9/L׍ ;Yatֆo W.B :1 LKz|ϡ;5Ҫjm}ǩ8uyeHϰ0oO<4Yz~u' <0|zLF]*`ΑbE8:qc}n 5bK~6Enb^TNixvL߷ En` t{*jhRq$flfCl[4omwvpowYNC""jcBM3MůnYωeYBXM~{522} S/cֱm LS})ݮm #9fT[PϏsɟg}D"&l-`xRJz'eOatkC1n{ dFIR'ƪ 8A (3^]p-'ZuS\!6&9^sߗED$hB)3CI;?G:?%3~KA 9ul$; `mwѥ0T,Q3]K+۲_mHK^V,YX{IV rTZᙝl_6kC8g' bn_4|G 8.zI_i;ϑGR zxSezb œ#'YU$IvEb.^.i^iylIbc7([GZ$]e=p9{~|Slj(ˍݷM wbp]p6=[RmZ}#-YDD8+ $$dD=F{ @ 3.gSڽ}2,X,BmCB,XZ0ۇooK ~ia`u -Svܲd|X.oR9~)=g`O(=GL4a$lό}wX8mO7z+}{W^lVlV0Lw4}"|5jQ[՚ѳlw fXZiPC`p|Ƕ;_<9UFAYF&o< ++8N`XqxyqO E)VR{.&x~ח  I8zjh}K\ê$,C;6:oV@$T >xB] R\XBnIK;FK~^ ˞DXe쏗I6y!H'3ԭ5ϷK i#'^

fb]Q Cs?c[d\>T!\Lv'K%v1Nڏ.d6 =Ʊ *KIW8^ .yl^Gږay%3>%# =nt3}_Uy0{g[IL jÊ'^ˌ%hһ&eYnK8>c>vM0+ k1c\~-b;ͩSfLI+Xs܉/Ib׀=ѕ[ѧy28{0V$mH÷{slڱod4j/a? ৒h\"tj?,8x[>DmN y8ٷ) 1'"e]1?=m)!ħ_߳-Ȉ=/H㼙p quD7,zӬh'3jS\CG%: |H:Ez0zqJ~X=2D_a .GJ?-'(YDDR5Ֆ(tg|&1mx~6s"=ε3eҨ|S$#eaZL$3m]݉i2wd͔FKsR%ADDw<Ʊ>O7&Sj ѓ^2?o ";~@+9~& 3T.W`#""OSlє'P`{п=P'fl:^+Θ[~L ^ƐX=VDDCDDDDDGQQQQQhhhhhQQQQQQhhhhhInEnT-DDED)>!\ CDD!ZDDDӧKUr=‰GMF[PI\T䏫J0^N0o $".e< -"8tᗡƅ?}fUt}V DD#Ҵ7b8<*#"\ "" n?Mxׯbwe!"v""""" """"" """"" """"" """"" """""-""""-""""-""""-""""-""""" """"" """"" """"" """"" """"" """""-""""-""""-""""-""""-""""" """"" """"" """"" """"" """"" """""EɚU(P-DDDD !kf'ۍqћ)0xVojWS(D˛͏P=?Qgcر"=q B9t1Y[ZU |/3,mm^aL<S =3.(u{XWw*oß-bmάp/""" ђX =>s\wi$/¨O q^t}.JySV$&zwTr$7Mߠ5i6hV2R.щF N`pӜ/%?_/D&gP)Ͱ."""rQ ' &ٟCo6-Z2pL6 h]8ZL0heb!?#c=h}>jѦSn6w׳cql?k@7F&~mb.Av^ """" ђ92V1F{$ENx1nbQ%):d'Dwz]\1IV7C4vb6bFBtzve/Rb=Ɏ^NGw(58NkYDDDtOtfܼXh=og=%l|7Mv raٳ-|d /7u_sPTN<[p厮FtfS9ʐ~f ϞƆcvFq*ˏqx3frQfINoJ,W2i[" c7p$e6P gBTBb'w5ܐI6?OX͘N8=͝PxIД=VpV5өidQ ZFDDDbFBADDDDyQQQQQË?W2zj('~؝mJ~6Z4oQ*dy#W^6G緕#iYyByVAL<|љ(ݤ5 ^͝٥(ot#n?wd o3Q62DK "Cނ#h6\هq^gMF۞JoEQѶWjIoףY÷F>_28Hx7#v9C*Ӝc&.spf'i^3O%Ν(=NbYU'y_BzPY-7,xnyv^jo8M{圙F^S ƞ$b!JHl!CO|"=ض=dUKQ%Ӎj3PKo4!ۻQ)Kz|ϡ;5ҪjͿ1]{MϤrYkE:֞ e9M]{M24-7d]Džtz=c:,f\P v T^16So4~;-qkzQֽȉegvp. _*iYw|8>aooB P{ 6s/@k}T*K9]Gǚ9n :;/ןc֟D}ۦ)ٙ2iU'ŷ?ᝬ?nodMcKN$f풩 YΩ˾n׃ӆb pfsjGWTǩr>Ni uvsL̬%au Ly G#Zj0B,TSLڶ&#W0f*p>VpM$l [SMg|,zqw JCZ'5L5sP%M]&8;уp@uIJq6#JI;jǔKd$Fڴh1؀u^@Oj1uS[{2Pєy)|@]&|o\i|-t.5zvиܨ~(s_v_8k}!)[֙,N-3H}OdM\ܞ޼DDC7i2?}/F] "~Է}H4c,]➞T~՗cq8]sjSq>(kHg''Lq,5!lCbL JɻϐR[n:q;'0$dnt~qщ_<1p`\-ᣎtg?Γ-7LH:v`Ze۾p8pRvhyJ؉^-_ [Ǔ _ uW\-IQS,^K֣l]IKRu])S߿u_+JIdhp:Dwzr tqt$'YM8:6}53vk\eg#$ڝ_VL]Z " g s)Œ5KS'L0Q8/N~߿Ωvs?'?ӰjӴi `d2Rؒ: 9(\9ƣ{,f'N+}]A Iq\̧Y|*]|I8nB<U,\ڋ/"Yyyl|޷9nq̮ٝLi0u"̄/E̯aVNGwG·q:2|5jQ[՚ѳlu8A4m`\3`Ɗ՚Z")]+6+;vݷΩ9t{0xj8ߦTqv6l6;G[ReOAm" ߟoNEE7ĤtW}mu8p``}5}7D!Z}̢wWY*lkWCX۔󖫗p raٳ @,1vOfӺq-JS:ˎh$MR8%gpp nzХɟ}̩e)Z+\_ -"cRL"~e /|L\rpbΩ5\NeYTϩ^S95W_^~mJuNf3`as|W}~\/Ksa Cpqz_V0,[Cdxy<(D6nx0V0.\\-OHE}Zaeo[;N/bl4diB:Ѻq]r>/N6u_֟W&RvK5׎ʮ$L>ϒ=e.EL߄q&06D8@%#Y n,6w6e\>T!\Lv'K%v1W IJR>CaԈvβ6ڇS~t!sap6G]U]J©aS0o&%21);]s3:՟S0sjW;ǜ?l-Ώz,@BI⹷өZ2ۺpnpKr*gMiQ[Bù#j1'`'caNqŧ4&"GB`n>cLb;7c8nB<A-/[|tާ1~ԇi}Yw#B"6ۅt_4o9_|"le١;OV}yq4)X8iqCFB:L[3eC]ߨ]zAſm6eĴ~' """@#Ow^)I2v+@?)^93IG(ua5Thy/Ә/5%- k\u'ɟjѪa?ˑe ?1j\Ԣ9DDDDDRȤ(D(DK*1Z4懑<`"g>uDծ $_O9h(ot<.|Қg.J֬B,y+L>OɂCFͩ_)'S[|BER:PMjG!kMӨO&7ɏ_^ qW%]I 8NŁ);e>)՝(8oStZ/ֱ\(/inR /Ĵ크XyuYGA>ߏaKX؆əPw,^-쒧ueHϰ0!{]\~c*x 2v)ZgC7gZڥrMp.VcţZg"5`xިivx[)r m+E*-υ,=C^ b/~#O0:U>8g/R+%cG{ˤv4}CK>دY[$g2^}[/cGG Ǝ,۳$)<(,q;#+FLeD2kueߓqK|@1|HɽSe46,$IO#4Xrzf~ĮYiJೳ)Jn,́YC};4=ң]uNn .l thRMs6bd-ȨoΒTƨm&sJɦ6)"R"vvX+ \T[Փ>;V8H|Fk_n} #&ԚIŰ~+'"QY:Pb1yo6XBԵ*QXiЈ?@q.fFt+K;1V5g,lBYu*Y|̗l"8NrcLv͘4lZ83rf5dTU(nMZT@D 8Eާzh2R})K8)O6% rntσz$Z Yќ}_C iT o X{/<.p&('=k~HD) :rnZ款Pϥ!LzG)Վ>}saX9電?ډ '%рIyB|)TTtFGuAET꾐o=.F6#dI Ib,Ӌ%Ka%a׆{oɜZN8}ϊm_r}r ŏ % œ~%sCv=6._U9Ժ{С'C1,:]g^Ԋo#:J|CO쑿' )6,ؚs{? ǘյ575`du&wZ)tʭOkqbUe>cCF{;$ֽAC"sNo@ͱ;5E ~ 1Ħ))V:@߼z<~Lj8RUΞ)'; ::R9O<ѮY3OOdͪV\ٗzLYGzt )V(U̒N 1U;Ң-foZ1Sk.@N}3J'VuaE,T_Ʊs;)u6-ws:ӑ֟AuƼyחsMj֡y1Z>Or0&0}QԥJc2$/wV Ԯ2r}7WVq~σy=Y/}(K kQ5Ss*&msYVY:ZT|>~X *KgY4[VkM WV% 'Fט-$ ߩV},JQ̗&_Xyq'ym;}B?b\e'@E h?PM3Vuќ{Q\l ~ "+l㏝\ǶԦ&SڕXu|>ϝ,+8< >eCPVV„oZӄr';N,MUϢ>>Hhfi( 4lQ "wx *2_Y١ XcF1%*}K '"g`s7Hhg OaD^fq(tL6WOc*CdB&jKz͠$2]:^7S`vy,}C;vq(ٟrL{jV_˶a$gRu+Fk1iNtosѺ|=\8IS54ֵ)ާ+߱לi\ $/&G`9g0VBh2nWd;J3Cg C}&ѐIQsfroFޘȐXb#)8n_)0#_2u XO0n嬠ȈX^WoHXxκ[GU͏o'[dch_m{Akc2" $/E۶3(Y|s~|8am̤*_֮W8[;W|poTP.$ջ3}"m*>]K>^6B|]R\eXYS.YƩBܻFC M*q*A ]޷BrF% B-T!JV wʙc(oVB ȊVLJdb67ѯUIN%q)ө9{]GӴ#D>Lc7rcE;MaJkSPh:6H|~)^`2_*ms!862g5̕_Y=7ꪸxҦҬ- γnp~Wh:}9_i80-_cv9EXF_>?~3~r/4;6F>'yd Z[=x@eMmY9@<~a֐X)2sO!+M9:}̎Yc3|)0СIW`Kds`xVxPm߀uN|{"M5i9>aXWNDt\,b2\g>mKvB%cա~n\͌&V6&v(lc:lMiffA)[vO7ʭηI+$PKda$p!^ 95FOLuŞ.a!)MzFLeD2kueߓqK|+K8SI$md,T277lwfWKqΕVX%cf3QC='‚zX_ȜO| YyEp1iشpf5̺7^?-BHCHRmReAԅG'BT;-υ2cEx8oh'6D~o] R<9GPbN9=Iz8lGc/\_SO{zr3ZCUuܴY}-KCl6#dI Ib,Ӌ%Ka%^ σ *J4Ϛ#k^Yќ}_C iT oRhb$SyI^ŵ@N3+?[K}Δ%9ftx00,ڰ`뽿ic͛:-BIJRRl ITeӤ "lueДsP>}CƠX˝:X~`>ǩ^Gi"‰֕VÒJ!{ x0Om/r3 F8KLRzzH?gɇv{~r!2!ebpyCgHu7 ow!{*KmWx֯+YZΓWkY*2 6GUOxjƘAAhP~9/! 6e Jvv? 0i| ?ƺ6%TQTa-LTxPp~ 9h4Ql۲p3Ih1.u7}Y30m8Il HlW]f0%EzZ POfq?Ѯ5)NH [rjxLt#ra'Ex;pĜxTYSe*6h9z!Vs^*m{"+S4ȿ87,J?Am^MƥKX˘@5+IDATBQhoQ;L**D7hڿ'Spž.+k}~Mt-˩3Z7ս 4yXdyjucx6%%iћ :x ObRrhgZf!&ruc]&3t z2/+KK'C`jfz"8NrcĪDЏ0ԞYJ{ݍ,v_8nӲqysP5-gff4û4L^s#-Bl>Js({tiCz>OIǽ;رN^ `LRYyV5)=)rpFw(ڢ,6)ew\J={lԐX;2e{$ϸ;5+&CoX`^H4ds7 m?gm0I\?}Wyφ3Hqbʮf |Ѵxɠ پ{z0 J@ (N RamX59a?Ƭqؼx-X_,NR|c'td˹i=2ZSC=J-B4r'5Z@STnL_y>Vq6$FK;+Qg3 ,Lu(H6!u*dNϧoUfwnKjp7o0,hR9gXp2O)%p(%fyh:7|W2Wlƞ.n^S3a!}UcJ'YT ~9_#rƀI:0/z&"Kcx-eڿ_*22(\D!pkըcU^֒D !/]&!{D$d>5M8oO{}Gyeקߤ^zu7L6H"A&$+]IsM*׮]NV.,1Fa=1* SHCتanv\;e߂eeSacc½g5_RΖb*HWVKgUeӌmͧG}Q7 ۃٿߦ"fLDH҃>U\l ~d iZFTf͔ bۜA2thQP^w?$B!^ٗ ҢJ904VaK:Rg&a.;д^üt;BE`~RuX5I}k*1Sc0$1Jj:.tSǭrKGBf.$`ZoIG`P,0xQS\y)xіeMAHqZ4Q2ͶL$UZ S^\0Jz _sBOOU0 %Kh!5ԡT>Axni{DO$2iNtoJ2^/hnsCo.;Ns8x9M%8E+0s6^Ū[XumZۙ'XP&='E"vQWwf *U\/]$BC00oY?ٔ_;`W-g\'3ď\W<~\rYQ3#V:Mx8j: 9U[j_.K\9 "PA6X~?%r)䪀c ~?ݭUIq>F.Ь;4O3p `z[m⺭){0dȦJAB7偁[w뭵 K]oC`Dk>3~rř7 pHYs  tIME  E@tEXtCommentCreated with GIMPW1IDATxOk`g7ݭT,ֶXDO*^EhA?x QQ(E VPP+n3^J҄un5d3! ~0 GbX"H@,bX" E,bX"E,bX$ w ?[rS8!ˎ2dX" e,1bʊ UoQWtT ڤ՞;< ې;iW`}hRm,_,donқ7"<~!"n fmn}M^:?f{5/b[7y\K: ȍtrjSigUpꊩg]nosYRzϡ78m \G,-²yǜ>YbuA4q-ՔdIҼ#|B}lC< sZI%k+Yi̊siR+GݘVVrĊMkpF ~wߐ8만 ґ-)K9lN Y=WlEyUf=n]fu6[ptTtF3k~-̧t sX ycB*mE4ҷ["u6ai#;UƝ>ӝ}SlCuEWrc/}]**\x=ލXs)\t 2Wj޻cۼEXӿW^{ ŏXd-|EC?HL wJpt 5OѦܨ _ItMVt4˭}2Ր X"X"E,E,bX bX" _+0^IENDB`bombadillo/VERSION0000644000175000017500000000000614211226262013552 0ustar nileshnilesh2.3.3 bombadillo/README.md0000644000175000017500000001764714211226262014004 0ustar nileshnilesh# Bombadillo - a non-web browser Bombadillo is a non-web browser for the terminal. ![a screenshot of the bombadillo browser](bombadillo-screenshot.png) Bombadillo features a full terminal user interface, vim-like keybindings, document pager, configurable settings, and a robust command selection. Currently, Bombadillo supports the following protocols as first class citizens: * gopher * gemini * finger * local (a user's file system) Support for the following protocols is also available via integration with 3rd party applications: * telnet * Links are opened in a telnet application run as a subprocess. * http/https * Web support is opt-in (turned off by default). * Links can be opened in a user's default web browser when in a graphical environment. * Web pages can be rendered directly in Bombadillo if [Lynx](https://lynx.invisible-island.net/), [w3m](http://w3m.sourceforge.net/), or [elinks](http://elinks.or.cz/) are installed on the system to handle the document parsing. ## Getting Started These instructions will get a copy of the project up and running on your local machine. The following only applies if you are building from source (rather than using a precompiled binary). ### Prerequisites You will need to have [Go](https://golang.org/) version >= 1.11. To use the Makefile you will need a make that is GNU Make compatible (sorry BSD folks) ### Building, Installing, Uninstalling Bombadillo installation uses `make`. It is also possible to use Go to build and install (i.e `go build`, `go install`), but this is not the recommended approach. Running `make` from the source code directory will build Bombadillo in the local directory. This is fine for testing or trying things out. For usage system-wide, and easy access to documentation, follow the installation instructions below. #### Basic Installation Most users will want to install using the following commands: ```shell git clone https://tildegit.org/sloum/bombadillo.git cd bombadillo sudo make install ``` *Note: the usage of `sudo` here will be system dependent. Most systems will require it for installation to `/usr/local/bin`.* You can then start Bombadillo by running the command: ```shell bombadillo ``` To familiarize yourself with the application, documentation is available by running the command: ```shell man bombadillo ``` #### Custom Installation ##### Configuration Options There are a number of default configuration options in the file `defaults.go`, allowing customisation of the default settings for users of Bombadillo. To use this feature, amend the `defaults.go` file as appropriate, then follow the standard install instructions. Full documentation for these options is contained within the `defaults.go` file. An administrator might use this to feature to set a default for all users of a system. Typically though, these options should not need changing, and a user may change most of these settings themselves once they start Bombadillo. The one option that can only be configured in `defaults.go` is `configlocation` which controls where `.bombadillo.ini` is stored. ##### Override Install Location The installation location can be overridden at compile time, which can be very useful for administrators wanting to set up Bombadillo on a multi-user machine. ```shell git clone https://tildegit.org/sloum/bombadillo.git cd bombadillo sudo make install PREFIX=/some/directory ``` There are two things to know about when using the above format: 1. The above would install Bombadillo to `/some/directory/bin`, _not_ to `/some/directory`. So you will want to make sure your `$PATH` is set accordingly. 2. Using the above will install the man page to `/some/directory/share/man/man1`, rather than its usual location. You will want to update your `manpath` accordingly. There are other overrides available - please review the [Makefile](Makefile) for more information. #### Uninstall If you used the Makefile to install Bombadillo then uninstalling is very simple. From the Bombadillo source folder run: ```shell sudo make uninstall ``` If you used a custom `PREFIX` value during install, you will need to supply it when uninstalling: ```shell sudo make uninstall PREFIX=/some/directory ``` Uninstall will clean up any build files, remove the installed binary, and remove the man page from the system. It will _not_ remove any directories created as a part of the installation, nor will it remove any Bombadillo user configuration files. #### Troubleshooting If you run `bombadillo` and get `bombadillo: command not found`, try running `make` from within the cloned repo. Then try: `./bombadillo`. If that works it means that the application is getting built correctly and the issue is likely in your path settings. Any errors during `make install` should be visible, and you will be able to see what command it failed on. ### Downloading If you would prefer to download a binary for your system, rather than build from source, please visit the [Bombadillo releases](http://bombadillo.colorfield.space/releases) page. Don't see your OS/architecture? Bombadillo can be built for use with any system that is supported as a target for the Go compiler (Linux, BSD, OS X, Plan 9). There is no explicit support for, or testing done for, Windows or Plan 9. The program should build on those systems, but you may encounter unexpected behaviors or incompatibilities. ### Documentation Bombadillo's primary documentation can be found in the man entry that installs with Bombadillo. To access it run `man bombadillo` after first installing Bombadillo. If for some reason that does not work, the document can be accessed directly from the source folder with `man ./bombadillo.1`. In addition to the man page, users can get information on Bombadillo on the web @ [http://bombadillo.colorfield.space](http://bombadillo.colorfield.space). Running the command `help` inside Bombadillo will navigate a user to the gopher server hosted at [bombadillo.colorfield.space](gopher://bombadillo.colorfield.space); specifically the user guide. ## Contributing Bombadillo development is largely handled by Sloum, with help from asdf, jboverf, and community input. There are many ways to contribute to Bombadillo, including a fair few that don't require knowledge of programming: - Try out the browser and let us know if you have a suggestion for improvement, or if you find a bug. - Read the documentation and let us know if something isn't well explained, or needs correction. - Maybe you have a cool logo or some art that you think would look nice. If you have something in mind, please reach out or [open an issue](https://tildegit.org/sloum/bombadillo/issues). We aim for simplicity and quality, and do our best to make Bombadillo useful to its users. Any proposals for change are reviewed by the maintainers with this in mind, and not every request will be accepted. Furthermore, this software is developed in our spare time for the good of all, and help is provided as best efforts. In general, we want to help! The maintainers use the [tildegit](https://tildegit.org) issues system to discuss new features, track bugs, and communicate with users regarding issues and suggestions. Pull requests should typically have an associated issue, and should target the `develop` branch. ## Development See [DEVELOPING.md](DEVELOPING.md) for information on how changes to Bombadillo are made, along with other technical information for developers. ## License This project is licensed under the GNU GPL version 3. See the [LICENSE](LICENSE) file for details. ## Releases Starting with version 2.0.0 releases into `master` will be version-tagged. Work done toward the next release will be created on work branches named for what they are doing and then merged into `develop` to be combined with other ongoing efforts before a release is merged into `master`. At present there is no specific release schedule. It will depend on the urgency of the work that makes its way into develop and will be up to the project maintainers' judgement when to release from `develop`. bombadillo/Makefile0000644000175000017500000000410714211226262014150 0ustar nileshnileshGOCMD := go BINARY := bombadillo PREFIX := /usr/local EXEC_PREFIX := ${PREFIX} BINDIR := ${EXEC_PREFIX}/bin DATAROOTDIR := ${PREFIX}/share MANDIR := ${DATAROOTDIR}/man MAN1DIR := ${MANDIR}/man1 test : GOCMD := go1.11.13 # Use a dateformat rather than -I flag since OSX # does not support -I. It also doesn't support # %:z - so settle for %z. BUILD_TIME := ${shell date "+%Y-%m-%dT%H:%M%z"} .PHONY: build build: ${GOCMD} build -o ${BINARY} .PHONY: install install: install-bin install-man install-desktop clean .PHONY: install-man install-man: bombadillo.1 gzip -c ./bombadillo.1 > ./bombadillo.1.gz install -d ${DESTDIR}${MAN1DIR} install -m 0644 ./bombadillo.1.gz ${DESTDIR}${MAN1DIR} .PHONY: install-desktop install-desktop: ifeq ($(shell uname), Linux) # These steps will not work on Darwin, Plan9, or Windows # They would likely work on BSD systems install -d ${DESTDIR}${DATAROOTDIR}/applications install -m 0644 ./bombadillo.desktop ${DESTDIR}${DATAROOTDIR}/applications install -d ${DESTDIR}${DATAROOTDIR}/pixmaps install -m 0644 ./bombadillo-icon.png ${DESTDIR}${DATAROOTDIR}/pixmaps -update-desktop-database 2> /dev/null else @echo "* Skipping protocol handler associations and desktop file creation for non-linux system *" endif .PHONY: install-bin install-bin: build install -d ${DESTDIR}${BINDIR} install -m 0755 ./${BINARY} ${DESTDIR}${BINDIR} .PHONY: clean clean: ${GOCMD} clean rm -f ./bombadillo.1.gz 2> /dev/null rm -f ./${BINARY}_* 2> /dev/null .PHONY: uninstall uninstall: clean rm -f ${DESTDIR}${MAN1DIR}/bombadillo.1.gz rm -f ${DESTDIR}${BINDIR}/${BINARY} rm -f ${DESTDIR}${DATAROOTDIR}/applications/bombadillo.desktop rm -f ${DESTDIR}${DATAROOTDIR}/pixmaps/bombadillo-icon.png -update-desktop-database 2> /dev/null .PHONY: release release: GOOS=linux GOARCH=amd64 ${GOCMD} build ${LDFLAGS} -o ${BINARY}_linux_64 GOOS=linux GOARCH=arm ${GOCMD} build ${LDFLAGS} -o ${BINARY}_linux_arm GOOS=linux GOARCH=386 ${GOCMD} build ${LDFLAGS} -o ${BINARY}_linux_32 GOOS=darwin GOARCH=amd64 ${GOCMD} build ${LDFLAGS} -o ${BINARY}_darwin_64 .PHONY: test test: clean build bombadillo/LICENSE0000644000175000017500000007733114211226262013526 0ustar nileshnilesh GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS bombadillo/DEVELOPING.md0000644000175000017500000000547714211226262014501 0ustar nileshnilesh# Developing Bombadillo ## Getting Started Following the standard install instructions should lead you to have nearly everything you need to commence development. The only additions to this are: - To be able to submit pull requests, you will need to fork this repository first. - The build process must be tested with Go 1.11 to ensure backward compatibility. This version can be installed as per the [Go install documentation](https://golang.org/doc/install#extra_versions). Check that changes build with this version using `make test`. - Linting must be performed on new changes using `gofmt` and [golangci-lint](https://github.com/golangci/golangci-lint) ## How changes are made A stable version of Bombadillo is kept in the default branch, so that people can easily clone the repo and get a good version of the software. New changes are implemented to the **develop** branch as **development releases**. Changes are implemented to the default branch when: - There are a set of changes in **develop** that are good enough to be considered stable. - This may be a **minor** set of changes for a **minor release**, or - a large **major** change for **major release**. - An urgent issue is identified in the stable version that requires an immediate **patch release**. ### Process for introducing a new change Before you begin, please refer to our [notes on contributing](README.md#contributing) to get an understanding of how new changes are initiated, the type of changes accepted and the review process. 1. Create a new feature branch based on the **develop** branch. 1. Raise a pull request (PR) targeting the current release branch (confirm this in the issue comments before proceeding). 1. The PR is reviewed. 1. If the PR is approved, it is merged. 1. The version number is incremented, along with any other release activity. ### Process for incrementing the version number The version number is incremented during a **development release**, **patch release**, and **minor** and **major releases**. This is primarily managed through git tags in the following way: ```shell # switch to the branch the release is being performed for git checkout branch # ensure everything is up to date git pull # get the commit ID for the recent merge git log # get the current version number (the highest number) git tag # for a development release, add the incremented version number to the commit-id, for example: git tag 2.0.2 abcdef # for releases to the default branch, this tag can also be added with annotations git tag 2.1.0 abdef -a "This version adds several new features..." ``` Releases to the default branch also include the following tasks: 1. The version number in the VERSION file is incremented and committed. 1. Release information should also be verified on the [tildegit releases page](https://tildegit.org/sloum/bombadillo/releases). bombadillo/.gitignore0000644000175000017500000000003514211226262014474 0ustar nileshnileshbombadillo *.asciinema *.swp