Merge pull request 'Release candidate for 2.3.0' (#147) from release-2.3.0 into develop

Alright. This is a big one for Gemini. I am going to merge in. :)
This commit is contained in:
Sloom Sloum Sluom IV 2020-05-24 12:42:27 -04:00
commit f793bdd806
11 changed files with 252 additions and 91 deletions

View File

@ -1 +1 @@
2.2.1 2.3.0

View File

@ -59,7 +59,7 @@ Displaying web content directly in \fBbombadillo\fP requires lynx, w3m or elinks
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. 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 .TP
.B .B
b b, h
Navigate back one place in your document history. Navigate back one place in your document history.
.TP .TP
.B .B
@ -71,7 +71,7 @@ d
Scroll down an amount corresponding to 75% of your terminal window height in the current document. Scroll down an amount corresponding to 75% of your terminal window height in the current document.
.TP .TP
.B .B
f f, l
Navigate forward one place in your document history. Navigate forward one place in your document history.
.TP .TP
.B .B
@ -95,6 +95,7 @@ n
Jump to next found text item. Jump to next found text item.
.TP .TP
.B .B
N
Jump to previous found text item. Jump to previous found text item.
.TP .TP
.B .B
@ -232,6 +233,10 @@ 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. 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 .TP
.B .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 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. 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 .TP

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv" "strconv"
@ -19,6 +18,7 @@ import (
"tildegit.org/sloum/bombadillo/http" "tildegit.org/sloum/bombadillo/http"
"tildegit.org/sloum/bombadillo/local" "tildegit.org/sloum/bombadillo/local"
"tildegit.org/sloum/bombadillo/telnet" "tildegit.org/sloum/bombadillo/telnet"
"tildegit.org/sloum/bombadillo/termios"
) )
//------------------------------------------------\\ //------------------------------------------------\\
@ -43,14 +43,7 @@ type client struct {
//--------------------------------------------------\\ //--------------------------------------------------\\
func (c *client) GetSizeOnce() { func (c *client) GetSizeOnce() {
cmd := exec.Command("stty", "size") var w, h = termios.GetWindowSize()
cmd.Stdin = os.Stdin
out, err := cmd.Output()
if err != nil {
cui.Exit(5, "Fatal error: Unable to retrieve terminal size")
}
var h, w int
_, _ = fmt.Sscan(string(out), &h, &w)
c.Height = h c.Height = h
c.Width = w c.Width = w
} }
@ -61,14 +54,7 @@ func (c *client) GetSize() {
c.Draw() c.Draw()
for { for {
cmd := exec.Command("stty", "size") var w, h = termios.GetWindowSize()
cmd.Stdin = os.Stdin
out, err := cmd.Output()
if err != nil {
cui.Exit(5, "Fatal error: Unable to retrieve terminal size")
}
var h, w int
_, _ = fmt.Sscan(string(out), &h, &w)
if h != c.Height || w != c.Width { if h != c.Height || w != c.Width {
c.Height = h c.Height = h
c.Width = w c.Width = w
@ -168,15 +154,15 @@ func (c *client) TakeControlInput() {
} else { } else {
c.goToLink(string(input)) c.goToLink(string(input))
} }
case 'j', 'J': case 'j':
// scroll down one line // scroll down one line
c.ClearMessage() c.ClearMessage()
c.Scroll(1) c.Scroll(1)
case 'k', 'K': case 'k':
// scroll up one line // scroll up one line
c.ClearMessage() c.ClearMessage()
c.Scroll(-1) c.Scroll(-1)
case 'q', 'Q': case 'q':
// quit bombadillo // quit bombadillo
cui.Exit(0, "") cui.Exit(0, "")
case 'g': case 'g':
@ -197,7 +183,7 @@ func (c *client) TakeControlInput() {
c.ClearMessage() c.ClearMessage()
distance := c.Height - c.Height/4 distance := c.Height - c.Height/4
c.Scroll(-distance) c.Scroll(-distance)
case 'b': case 'b', 'h':
// go back // go back
c.ClearMessage() c.ClearMessage()
err := c.PageState.NavigateHistory(-1) err := c.PageState.NavigateHistory(-1)
@ -222,7 +208,7 @@ func (c *client) TakeControlInput() {
// open the bookmarks browser // open the bookmarks browser
c.BookMarks.ToggleOpen() c.BookMarks.ToggleOpen()
c.Draw() c.Draw()
case 'f', 'F': case 'f', 'l':
// go forward // go forward
c.ClearMessage() c.ClearMessage()
err := c.PageState.NavigateHistory(1) err := c.PageState.NavigateHistory(1)
@ -470,6 +456,8 @@ func (c *client) doCommandAs(action string, values []string) {
c.Options[values[0]] = lowerCaseOpt(values[0], val) c.Options[values[0]] = lowerCaseOpt(values[0], val)
if values[0] == "tlskey" || values[0] == "tlscertificate" { if values[0] == "tlskey" || values[0] == "tlscertificate" {
c.Certs.LoadCertificate(c.Options["tlscertificate"], c.Options["tlskey"]) c.Certs.LoadCertificate(c.Options["tlscertificate"], c.Options["tlskey"])
} else if values[0] == "geminiblocks" {
gemini.BlockBehavior = c.Options[values[0]]
} else if values[0] == "configlocation" { } else if values[0] == "configlocation" {
c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true) c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true)
c.DrawMessage() c.DrawMessage()
@ -676,7 +664,7 @@ func (c *client) doLinkCommand(action, target string) {
} }
func (c *client) search(query, url, question string) { func (c *client) search(query, uri, question string) {
var entry string var entry string
var err error var err error
if query == "" { if query == "" {
@ -700,22 +688,32 @@ func (c *client) search(query, url, question string) {
} else { } else {
entry = query entry = query
} }
if url == "" { if uri == "" {
url = c.Options["searchengine"] uri = c.Options["searchengine"]
} }
u, err := MakeUrl(url) u, err := MakeUrl(uri)
if err != nil { if err != nil {
c.SetMessage("The search url is not a valid url", true) c.SetMessage("The search url is not valid", true)
c.DrawMessage() c.DrawMessage()
return return
} }
var rootUrl string
switch u.Scheme { switch u.Scheme {
case "gopher": case "gopher":
go c.Visit(fmt.Sprintf("%s\t%s", u.Full, entry)) if ind := strings.Index(entry, "\t"); ind >= 0 {
rootUrl = u.Full[:ind]
} else {
rootUrl = u.Full
}
c.Visit(fmt.Sprintf("%s\t%s", rootUrl, entry))
case "gemini": case "gemini":
// TODO url escape the entry variable if ind := strings.Index(entry, "?"); ind >= 0 {
escapedEntry := entry rootUrl = u.Full[:ind]
go c.Visit(fmt.Sprintf("%s?%s", u.Full, escapedEntry)) } else {
rootUrl = u.Full
}
// escapedEntry := url.QueryEscape(entry) // TODO confirm expected behavior re: escaping
c.Visit(fmt.Sprintf("%s?%s", rootUrl, entry))
case "http", "https": case "http", "https":
c.Visit(u.Full) c.Visit(u.Full)
default: default:
@ -977,8 +975,10 @@ func (c *client) handleGemini(u Url) {
go saveConfig() go saveConfig()
switch capsule.Status { switch capsule.Status {
case 1: case 1:
// Query
c.search("", u.Full, capsule.Content) c.search("", u.Full, capsule.Content)
case 2: case 2:
// Success
if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") { if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") {
pg := MakePage(u, capsule.Content, capsule.Links) pg := MakePage(u, capsule.Content, capsule.Links)
pg.FileType = capsule.MimeMaj pg.FileType = capsule.MimeMaj
@ -996,14 +996,21 @@ func (c *client) handleGemini(u Url) {
c.saveFileFromData(capsule.Content, filename) c.saveFileFromData(capsule.Content, filename)
} }
case 3: case 3:
c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false) // Redirect
c.DrawMessage() lowerRedirect := strings.ToLower(capsule.Content)
ch := cui.Getch() lowerOriginal := strings.ToLower(u.Full)
if ch == 'y' || ch == 'Y' { if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" {
c.Visit(capsule.Content) c.Visit(capsule.Content)
} else { } else {
c.SetMessage("Redirect aborted", false) c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false)
c.DrawMessage() c.DrawMessage()
ch := cui.Getch()
if ch == 'y' || ch == 'Y' {
c.Visit(capsule.Content)
} else {
c.SetMessage("Redirect aborted", false)
c.DrawMessage()
}
} }
} }
} }

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"tildegit.org/sloum/bombadillo/termios"
) )
var Shapes = map[string]string{ var Shapes = map[string]string{
@ -55,16 +57,17 @@ func Exit(exitCode int, msg string) {
// InitTerm sets the terminal modes appropriate for Bombadillo // InitTerm sets the terminal modes appropriate for Bombadillo
func InitTerm() { func InitTerm() {
SetCharMode() termios.SetCharMode()
Tput("smcup") // use alternate screen Tput("smcup") // use alternate screen
Tput("rmam") // turn off line wrapping Tput("rmam") // turn off line wrapping
fmt.Print("\033[?25l") // hide cursor
} }
// CleanupTerm reverts changs to terminal mode made by InitTerm // CleanupTerm reverts changs to terminal mode made by InitTerm
func CleanupTerm() { func CleanupTerm() {
moveCursorToward("down", 500) moveCursorToward("down", 500)
moveCursorToward("right", 500) moveCursorToward("right", 500)
SetLineMode() termios.SetLineMode()
fmt.Print("\n") fmt.Print("\n")
fmt.Print("\033[?25h") // reenables cursor blinking fmt.Print("\033[?25h") // reenables cursor blinking
@ -98,7 +101,7 @@ func Getch() rune {
} }
func GetLine(prefix string) (string, error) { func GetLine(prefix string) (string, error) {
SetLineMode() termios.SetLineMode()
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
fmt.Print(prefix) fmt.Print(prefix)
@ -107,32 +110,10 @@ func GetLine(prefix string) (string, error) {
return "", err return "", err
} }
SetCharMode() termios.SetCharMode()
return text[:len(text)-1], nil return text[:len(text)-1], nil
} }
func SetCharMode() {
cmd := exec.Command("stty", "cbreak", "-echo")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err := cmd.Run()
if err != nil {
panic(err)
}
fmt.Print("\033[?25l")
}
func SetLineMode() {
cmd := exec.Command("stty", "-cbreak", "echo")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err := cmd.Run()
if err != nil {
panic(err)
}
}
func Tput(opt string) { func Tput(opt string) {
cmd := exec.Command("tput", opt) cmd := exec.Command("tput", opt)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin

View File

@ -47,6 +47,7 @@ var defaultOptions = map[string]string{
"configlocation": xdgConfigPath(), "configlocation": xdgConfigPath(),
"defaultscheme": "gopher", // "gopher", "gemini", "http", "https" "defaultscheme": "gopher", // "gopher", "gemini", "http", "https"
"geminiblocks": "block", // "block", "alt", "neither", "both"
"homeurl": "gopher://bombadillo.colorfield.space:70/1/user-guide.map", "homeurl": "gopher://bombadillo.colorfield.space:70/1/user-guide.map",
"savelocation": homePath(), "savelocation": homePath(),
"searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs", "searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs",

View File

@ -24,6 +24,8 @@ type TofuDigest struct {
ClientCert tls.Certificate ClientCert tls.Certificate
} }
var BlockBehavior = "block"
//------------------------------------------------\\ //------------------------------------------------\\
// + + + R E C E I V E R S + + + \\ // + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\ //--------------------------------------------------\\
@ -49,8 +51,8 @@ func (t *TofuDigest) Purge(host string) error {
return fmt.Errorf("Invalid host %q", host) return fmt.Errorf("Invalid host %q", host)
} }
func (t *TofuDigest) Add(host, hash string) { func (t *TofuDigest) Add(host, hash string, time int64) {
t.certs[strings.ToLower(host)] = hash t.certs[strings.ToLower(host)] = fmt.Sprintf("%s|%d", hash, time)
} }
func (t *TofuDigest) Exists(host string) bool { func (t *TofuDigest) Exists(host string) bool {
@ -67,12 +69,11 @@ func (t *TofuDigest) Find(host string) (string, error) {
return "", fmt.Errorf("Invalid hostname, no key saved") return "", fmt.Errorf("Invalid hostname, no key saved")
} }
func (t *TofuDigest) Match(host string, cState *tls.ConnectionState) error { func (t *TofuDigest) Match(host, localCert string, cState *tls.ConnectionState) error {
host = strings.ToLower(host)
now := time.Now() now := time.Now()
for _, cert := range cState.PeerCertificates { for _, cert := range cState.PeerCertificates {
if t.certs[host] != hashCert(cert.Raw) { if localCert != hashCert(cert.Raw) {
continue continue
} }
@ -118,13 +119,40 @@ func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error {
continue continue
} }
t.Add(host, hashCert(cert.Raw)) t.Add(host, hashCert(cert.Raw), cert.NotAfter.Unix())
return nil return nil
} }
return fmt.Errorf(reasons.String()) 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 { func (t *TofuDigest) IniDump() string {
if len(t.certs) < 1 { if len(t.certs) < 1 {
return "" return ""
@ -176,9 +204,11 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) {
return "", fmt.Errorf("Insecure, no certificates offered by server") return "", fmt.Errorf("Insecure, no certificates offered by server")
} }
if td.Exists(host) { localCert, localTs, err := td.GetCertAndTimestamp(host)
if localTs > 0 {
// See if we have a matching cert // See if we have a matching cert
err := td.Match(host, &connState) err := td.Match(host, localCert, &connState)
if err != nil && err.Error() != "EXP" { if err != nil && err.Error() != "EXP" {
// If there is no match and it isnt because of an expiration // If there is no match and it isnt because of an expiration
// just return the error // just return the error
@ -339,16 +369,23 @@ func parseGemini(b, rootUrl, currentUrl string) (string, []string) {
splitContent := strings.Split(b, "\n") splitContent := strings.Split(b, "\n")
links := make([]string, 0, 10) links := make([]string, 0, 10)
inPreBlock := false
outputIndex := 0 outputIndex := 0
for i, ln := range splitContent { for i, ln := range splitContent {
splitContent[i] = strings.Trim(ln, "\r\n") splitContent[i] = strings.Trim(ln, "\r\n")
if ln == "```" { isPreBlockDeclaration := strings.HasPrefix(ln, "```")
// By continuing we create a variance between i and outputIndex if isPreBlockDeclaration && !inPreBlock && (BlockBehavior == "both" || BlockBehavior == "alt") {
// the other branches here will write to the outputIndex rather inPreBlock = !inPreBlock
// than i, thus removing these lines while itterating without alt := strings.TrimSpace(ln)
// needing mroe allocations. if len(alt) > 3 {
continue alt = strings.TrimSpace(alt[3:])
} else if len([]rune(ln)) > 3 && ln[:2] == "=>" { splitContent[outputIndex] = fmt.Sprintf("[ %s ]", alt)
outputIndex++
}
} else if isPreBlockDeclaration {
inPreBlock = !inPreBlock
} else if len([]rune(ln)) > 3 && ln[:2] == "=>" && !inPreBlock {
var link, decorator string var link, decorator string
subLn := strings.Trim(ln[2:], "\r\n\t \a") subLn := strings.Trim(ln[2:], "\r\n\t \a")
splitPoint := strings.IndexAny(subLn, " \t") splitPoint := strings.IndexAny(subLn, " \t")
@ -370,6 +407,9 @@ func parseGemini(b, rootUrl, currentUrl string) (string, []string) {
splitContent[outputIndex] = fmt.Sprintf("%-5s %s", linknum, decorator) splitContent[outputIndex] = fmt.Sprintf("%-5s %s", linknum, decorator)
outputIndex++ outputIndex++
} else { } else {
if inPreBlock && (BlockBehavior == "alt" || BlockBehavior == "neither") {
continue
}
splitContent[outputIndex] = ln splitContent[outputIndex] = ln
outputIndex++ outputIndex++
} }
@ -381,18 +421,38 @@ func handleRelativeUrl(u, root, current string) string {
if len(u) < 1 { if len(u) < 1 {
return u return u
} }
currentIsDir := (current[len(current)-1] == '/')
if u[0] == '/' { if u[0] == '/' {
return fmt.Sprintf("%s%s", root, u) return fmt.Sprintf("%s%s", root, u)
} else if strings.HasPrefix(u, "../") {
currentDir := strings.LastIndex(current, "/")
if currentIsDir {
upOne := strings.LastIndex(current[:currentDir], "/")
dirRoot := current[:upOne]
return dirRoot + u[2:]
}
return current[:currentDir] + u[2:]
}
if strings.HasPrefix(u, "./") {
if len(u) == 2 {
return current
}
u = u[2:]
}
if currentIsDir {
indPrevDir := strings.LastIndex(current[:len(current)-1], "/")
if indPrevDir < 9 {
return current + u
}
return current[:indPrevDir+1] + u
} }
ind := strings.LastIndex(current, "/") ind := strings.LastIndex(current, "/")
if ind < 10 {
return fmt.Sprintf("%s/%s", root, u)
}
current = current[:ind+1] current = current[:ind+1]
return fmt.Sprintf("%s%s", current, u) return current + u
} }
func hashCert(cert []byte) string { func hashCert(cert []byte) string {

25
main.go
View File

@ -25,12 +25,14 @@ import (
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"syscall" "syscall"
"time"
"tildegit.org/sloum/bombadillo/config" "tildegit.org/sloum/bombadillo/config"
"tildegit.org/sloum/bombadillo/cui" "tildegit.org/sloum/bombadillo/cui"
_ "tildegit.org/sloum/bombadillo/gemini" "tildegit.org/sloum/bombadillo/gemini"
) )
var version string var version string
@ -66,6 +68,7 @@ func validateOpt(opt, val string) bool {
"theme": []string{"normal", "inverse", "color"}, "theme": []string{"normal", "inverse", "color"},
"defaultscheme": []string{"gopher", "gemini", "http", "https"}, "defaultscheme": []string{"gopher", "gemini", "http", "https"},
"showimages": []string{"true", "false"}, "showimages": []string{"true", "false"},
"geminiblocks": []string{"block", "neither", "alt", "both"},
} }
opt = strings.ToLower(opt) opt = strings.ToLower(opt)
@ -84,7 +87,7 @@ func validateOpt(opt, val string) bool {
func lowerCaseOpt(opt, val string) string { func lowerCaseOpt(opt, val string) string {
switch opt { switch opt {
case "webmode", "theme", "defaultscheme", "showimages": case "webmode", "theme", "defaultscheme", "showimages", "geminiblocks":
return strings.ToLower(val) return strings.ToLower(val)
default: default:
return val return val
@ -121,6 +124,9 @@ func loadConfig() {
if _, ok := bombadillo.Options[lowerkey]; ok { if _, ok := bombadillo.Options[lowerkey]; ok {
if validateOpt(lowerkey, v.Value) { if validateOpt(lowerkey, v.Value) {
bombadillo.Options[lowerkey] = v.Value bombadillo.Options[lowerkey] = v.Value
if lowerkey == "geminiblocks" {
gemini.BlockBehavior = v.Value
}
} else { } else {
bombadillo.Options[lowerkey] = defaultOptions[lowerkey] bombadillo.Options[lowerkey] = defaultOptions[lowerkey]
} }
@ -132,7 +138,20 @@ func loadConfig() {
} }
for _, v := range settings.Certs { for _, v := range settings.Certs {
bombadillo.Certs.Add(v.Key, v.Value) // Remove expired certs
vals := strings.SplitN(v.Value, "|", -1)
if len(vals) < 2 {
continue
}
ts, err := strconv.ParseInt(vals[1], 10, 64)
now := time.Now()
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)
} }
} }

10
page.go
View File

@ -70,7 +70,9 @@ func (p *Page) WrapContent(width int, color bool) {
p.RenderImage(width) p.RenderImage(width)
return return
} }
width = min(width, 100)
counter := 0 counter := 0
spacer := " "
var content strings.Builder var content strings.Builder
var esc strings.Builder var esc strings.Builder
escape := false escape := false
@ -124,7 +126,6 @@ func (p *Page) WrapContent(width int, color bool) {
content.WriteRune('\n') content.WriteRune('\n')
counter = 0 counter = 0
if p.Location.Mime == "1" { if p.Location.Mime == "1" {
spacer := " "
content.WriteString(spacer) content.WriteString(spacer)
counter += len(spacer) counter += len(spacer)
} }
@ -188,3 +189,10 @@ func MakePage(url Url, content string, links []string) Page {
p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0, "", 40, false} p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0, "", 40, false}
return p return p
} }
func min(a, b int) int {
if a < b {
return a
}
return b
}

10
termios/consts_linux.go Normal file
View File

@ -0,0 +1,10 @@
// +build linux
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TCGETS
setTermiosIoctl = syscall.TCSETS
)

View File

@ -0,0 +1,10 @@
// +build !linux
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TIOCGETA
setTermiosIoctl = syscall.TIOCSETAF
)

60
termios/termios.go Normal file
View File

@ -0,0 +1,60 @@
package 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)
}