[mike@mwxm4]
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -18,71 +18,102 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/eknkc/basex"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mgutz/ansi"
|
||||
"github.com/minio/selfupdate"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var tbversion = "0.5.0"
|
||||
var tbversion = "0.7.0"
|
||||
|
||||
var HTTPTIMEOUT = 3 * time.Second // version.txt / checksums.txt - must never stall a build
|
||||
var DOWNLOADTIMEOUT = 10 * time.Minute // the binary itself, http.Client.Timeout covers the whole body
|
||||
|
||||
var CHRS = "VW9IdGJ6eXh1T25DRHdrc2M5MlhOQVNQcEJFWnJhWVY2ZEowaFJLdmoxNUdxVDRJZkZpTTdRZW0zTFc4Z2w="
|
||||
var LR = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func checkforupdate(URL string) { // ----------------------------------------------------- check for new version
|
||||
prg := prgname()
|
||||
resp, err := http.Get(URL+"/version.txt")
|
||||
if err == nil {
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
if scanner.Scan() {
|
||||
lversion := strings.TrimSpace(scanner.Text())
|
||||
sv_version, err := semver.NewVersion(version)
|
||||
if err == nil {
|
||||
sv_lversion, err := semver.NewVersion(lversion)
|
||||
if err == nil {
|
||||
if (sv_lversion.GreaterThan(sv_version)) {
|
||||
ans:=Yesno(SF("new '%s' version found (%s -> %s), update now?",
|
||||
prg,sv_version,sv_lversion),true,false);
|
||||
if (ans) {
|
||||
updateurl:=SF("%s/%s_%s_%s_%s",URL,prg,lversion,runtime.GOOS,runtime.GOARCH)
|
||||
|
||||
if err := doupdate(updateurl); err != nil {
|
||||
PO(SF("Update failed: %v\n", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
PO("Update successful!","please run your last command again")
|
||||
os.Exit(0)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.Body.Close()
|
||||
client := &http.Client{Timeout: HTTPTIMEOUT}
|
||||
|
||||
body, err := fetch(client, URL+"/version.txt", 1<<20) // update server unreachable: never block the build
|
||||
if err != nil { return }
|
||||
|
||||
lversion := strings.TrimSpace(string(body)) // exactly as published - it names the artifact
|
||||
sv_version, err := semver.NewVersion(version)
|
||||
if err != nil { return }
|
||||
sv_lversion, err := semver.NewVersion(lversion)
|
||||
if err != nil { return }
|
||||
if !sv_lversion.GreaterThan(sv_version) { return }
|
||||
|
||||
if !Yesno(SF("new '%s' version found (%s -> %s), update now?",prg,sv_version,lversion),true,false) {
|
||||
return
|
||||
}
|
||||
|
||||
target := SF("%s_%s_%s_%s",prg,lversion,runtime.GOOS,runtime.GOARCH)
|
||||
if runtime.GOOS == "windows" { target += ".exe" }
|
||||
|
||||
sum, err := getchecksum(client, URL+"/checksums.txt", target) // no checksum -> no update
|
||||
if err != nil {
|
||||
PE("update aborted", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := doupdate(&http.Client{Timeout: DOWNLOADTIMEOUT}, URL+"/"+target, sum); err != nil {
|
||||
PE("update failed", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
PO("update successful","please run your last command again")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func doupdate(url string) error { // ----------------------------------------------------------------- do update
|
||||
resp, err := http.Get(url)
|
||||
func doupdate(client *http.Client, url string, checksum []byte) error { // --------------------------- do update
|
||||
resp, err := client.Get(url)
|
||||
if err != nil { return err }
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK { return fmt.Errorf("server returned status: %v", resp.Status) }
|
||||
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
|
||||
if err != nil { return err }
|
||||
return nil
|
||||
if resp.StatusCode != http.StatusOK { return SE("server returned status: %v", resp.Status) }
|
||||
return selfupdate.Apply(resp.Body, selfupdate.Options{Checksum: checksum})
|
||||
}
|
||||
|
||||
func checkaccess(NETS []string) { // ------------------------------------------------- check ip net based access
|
||||
func getchecksum(client *http.Client, url string, name string) ([]byte, error) { // -- sha256 of a published file
|
||||
body, err := fetch(client, url, 1<<20)
|
||||
if err != nil { return nil, SE("no checksums published (%v)", err) }
|
||||
|
||||
for _, line := range strings.Split(string(body), "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) == 2 && f[1] == name {
|
||||
sum, err := hex.DecodeString(f[0])
|
||||
if err != nil { return nil, SE("bad checksum for %s", name) }
|
||||
return sum, nil
|
||||
}
|
||||
}
|
||||
return nil, SE("no checksum for %s", name)
|
||||
}
|
||||
|
||||
func fetch(client *http.Client, url string, max int64) ([]byte, error) { // ------------------- fetch a small file
|
||||
resp, err := client.Get(url)
|
||||
if err != nil { return nil, err }
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK { return nil, SE("server returned status: %v", resp.Status) }
|
||||
return io.ReadAll(io.LimitReader(resp.Body, max))
|
||||
}
|
||||
|
||||
func checkaccess(NETS []string) { // ------------------------------------------------- check ip net based access
|
||||
match:=0;
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil { PE("Error getting addresses", err.Error()); os.Exit(1) }
|
||||
|
||||
for _, validnet := range NETS {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil { PE("Error getting addresses"); os.Exit(1) }
|
||||
_, ipNet, err := net.ParseCIDR(validnet)
|
||||
if err != nil { PE("invalid network", validnet); continue }
|
||||
for _, address := range addrs {
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
@@ -102,8 +133,10 @@ func Enc(str string) string { // -----------------------------------------------
|
||||
}
|
||||
|
||||
func Dec(str string) string { // ----------------------------------------------------------------- decode string
|
||||
enc, _ := basex.NewEncoding(Db64(CHRS))
|
||||
b, _ := enc.Decode(str)
|
||||
enc, err := basex.NewEncoding(Db64(CHRS))
|
||||
if err != nil { return "" }
|
||||
b, err := enc.Decode(str)
|
||||
if err != nil || len(b) < 2 { return "" }
|
||||
return string(b[2:])
|
||||
}
|
||||
|
||||
@@ -193,7 +226,8 @@ func GETid(n int) string { // --------------------------------------------------
|
||||
|
||||
func Mytable(rows *sql.Rows) []map[string]interface{} { // ----------------------------- load mysql result table
|
||||
defer rows.Close()
|
||||
columns, _ := rows.Columns()
|
||||
columns, err := rows.Columns()
|
||||
if err != nil { return nil }
|
||||
count := len(columns)
|
||||
tableData := make([]map[string]interface{}, 0)
|
||||
values := make([]interface{}, count)
|
||||
@@ -202,7 +236,7 @@ func Mytable(rows *sql.Rows) []map[string]interface{} { // ---------------------
|
||||
for i := 0; i < count; i++ {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
rows.Scan(valuePtrs...)
|
||||
if err := rows.Scan(valuePtrs...); err != nil { break }
|
||||
entry := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
var v interface{}
|
||||
@@ -248,6 +282,7 @@ func Isflagpassed(name string) bool { // ---------------------------------------
|
||||
}
|
||||
|
||||
func Body(r *http.Response) string { // ------------------------------------------------------------ http body
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err == nil {
|
||||
return string(body)
|
||||
@@ -281,14 +316,17 @@ func P(a ...any) (n int, err error) { return fmt.Fprintln(os.Std
|
||||
func PN(a ...any) (n int, err error) { return fmt.Fprint(os.Stdout, a...) }
|
||||
func PF(format string, a ...any) (n int, err error) { return fmt.Fprintf(os.Stdout, format, a...) }
|
||||
func SF(format string, a ...any) string { return fmt.Sprintf(format, a...) }
|
||||
func SE(format string, a ...any) error { return fmt.Errorf(format, a...) }
|
||||
|
||||
func PE(msg ...string) (n int, err error) {
|
||||
if (len(msg)==2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Crb("ERROR"), Cwb(msg[0]),msg[1]) }
|
||||
if (len(msg)==0) { return 0, nil }
|
||||
if (len(msg)>=2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Crb("ERROR"), Cwb(msg[0]),msg[1]) }
|
||||
return fmt.Fprintf(os.Stdout, "%s: %s\n", Crb("ERROR"), Cwb(msg[0]))
|
||||
}
|
||||
func PO(msg ...string) (n int, err error) {
|
||||
if (len(msg)==2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Cgb("OK"), Cwb(msg[0]),msg[1]) }
|
||||
return fmt.Fprintf(os.Stdout, "%s: %s\n", Cgb("OK"), Cwb(msg[0]))
|
||||
if (len(msg)==0) { return 0, nil }
|
||||
if (len(msg)>=2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Cgb("OK"), Cwb(msg[0]),msg[1]) }
|
||||
return fmt.Fprintf(os.Stdout, "%s: %s\n", Cgb("OK"), Cwb(msg[0]))
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------ regular expression
|
||||
@@ -306,13 +344,16 @@ func Shortstr(s string, length int) string {
|
||||
if len(runes) <= length {
|
||||
return s
|
||||
}
|
||||
if length <= 2 {
|
||||
return ".."
|
||||
}
|
||||
return string(runes[:length-2]) + ".."
|
||||
}
|
||||
|
||||
func SR(str string, n int) string { return strings.Repeat(str, n) }
|
||||
|
||||
func RemoveAllMatches(slice []string, target string) []string { // remove matching string from array
|
||||
result := slice[:0]
|
||||
result := make([]string, 0, len(slice)) // never modify the callers slice
|
||||
for _, v := range slice {
|
||||
if v != target {
|
||||
result = append(result, v)
|
||||
@@ -330,7 +371,7 @@ func prgname() string {
|
||||
return ""
|
||||
}
|
||||
exename := filepath.Base(exepath)
|
||||
return exename
|
||||
return strings.TrimSuffix(exename, ".exe") // windows: gbld.exe -> gbld
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------ viper abbrevations
|
||||
@@ -342,21 +383,118 @@ func VB(key string) bool { return viper.GetBool(key) }
|
||||
func VI(key string) int { return viper.GetInt(key) }
|
||||
|
||||
// ------------------------------------------------------------------------------------------------- text colors
|
||||
//
|
||||
// catppuccin mocha - https://terminalcolors.com/themes/catppuccin/mocha/
|
||||
// the palette is emitted as 24 bit color. terminals that do not announce truecolor via COLORTERM
|
||||
// fall back to the basic ansi colors, so their own theme keeps deciding. NO_COLOR is honoured.
|
||||
|
||||
var Cr func(...interface{}) string = color.New(color.FgRed).SprintFunc()
|
||||
var Cg func(...interface{}) string = color.New(color.FgGreen).SprintFunc()
|
||||
var Cy func(...interface{}) string = color.New(color.FgYellow).SprintFunc()
|
||||
var Cb func(...interface{}) string = color.New(color.FgBlue).SprintFunc()
|
||||
var Cm func(...interface{}) string = color.New(color.FgMagenta).SprintFunc()
|
||||
var Cc func(...interface{}) string = color.New(color.FgCyan).SprintFunc()
|
||||
var Cw func(...interface{}) string = color.New(color.FgWhite).SprintFunc()
|
||||
type rgb struct{ r, g, b int }
|
||||
|
||||
var Crb func(...interface{}) string = color.New(color.Bold, color.FgRed).SprintFunc()
|
||||
var Cgb func(...interface{}) string = color.New(color.Bold, color.FgGreen).SprintFunc()
|
||||
var Cyb func(...interface{}) string = color.New(color.Bold, color.FgYellow).SprintFunc()
|
||||
var Cbb func(...interface{}) string = color.New(color.Bold, color.FgBlue).SprintFunc()
|
||||
var Cmb func(...interface{}) string = color.New(color.Bold, color.FgMagenta).SprintFunc()
|
||||
var Ccb func(...interface{}) string = color.New(color.Bold, color.FgCyan).SprintFunc()
|
||||
var Cwb func(...interface{}) string = color.New(color.Bold, color.FgWhite).SprintFunc()
|
||||
var ( // ------------------------------------------------------------------------- catppuccin mocha palette
|
||||
CatRosewater = rgb{0xf5, 0xe0, 0xdc}
|
||||
CatFlamingo = rgb{0xf2, 0xcd, 0xcd}
|
||||
CatPink = rgb{0xf5, 0xc2, 0xe7}
|
||||
CatMauve = rgb{0xcb, 0xa6, 0xf7}
|
||||
CatRed = rgb{0xf3, 0x8b, 0xa8}
|
||||
CatMaroon = rgb{0xeb, 0xa0, 0xac}
|
||||
CatPeach = rgb{0xfa, 0xb3, 0x87}
|
||||
CatYellow = rgb{0xf9, 0xe2, 0xaf}
|
||||
CatGreen = rgb{0xa6, 0xe3, 0xa1}
|
||||
CatTeal = rgb{0x94, 0xe2, 0xd5}
|
||||
CatSky = rgb{0x89, 0xdc, 0xeb}
|
||||
CatSapphire = rgb{0x74, 0xc7, 0xec}
|
||||
CatBlue = rgb{0x89, 0xb4, 0xfa}
|
||||
CatLavender = rgb{0xb4, 0xbe, 0xfe}
|
||||
CatText = rgb{0xcd, 0xd6, 0xf4}
|
||||
CatSubtext1 = rgb{0xba, 0xc2, 0xde}
|
||||
CatSubtext0 = rgb{0xa6, 0xad, 0xc8}
|
||||
CatOverlay1 = rgb{0x7f, 0x84, 0x9c}
|
||||
CatSurface1 = rgb{0x45, 0x47, 0x5a}
|
||||
)
|
||||
|
||||
var catansi = map[string]rgb{ // ------------------------------- how mocha fills the ansi slots (bright = normal)
|
||||
"black": CatSurface1,
|
||||
"red": CatRed,
|
||||
"green": CatGreen,
|
||||
"yellow": CatYellow,
|
||||
"blue": CatBlue,
|
||||
"magenta": CatPink,
|
||||
"cyan": CatTeal,
|
||||
"white": CatSubtext1,
|
||||
"default": CatText,
|
||||
}
|
||||
|
||||
func truecolor() bool { // ------------------------------------------ does the terminal understand 24 bit color
|
||||
ct := strings.ToLower(os.Getenv("COLORTERM"))
|
||||
return ct == "truecolor" || ct == "24bit"
|
||||
}
|
||||
|
||||
func cfunc(c rgb, fallback color.Attribute, attrs ...color.Attribute) func(...interface{}) string {
|
||||
col := color.New(attrs...)
|
||||
if truecolor() {
|
||||
col.AddRGB(c.r, c.g, c.b)
|
||||
} else {
|
||||
col.Add(fallback)
|
||||
}
|
||||
return col.SprintFunc()
|
||||
}
|
||||
|
||||
// mocha maps its ansi slots to red/green/yellow/blue/pink/teal, bright is identical to normal -
|
||||
// so the bold variants keep the hue and only add weight, exactly as before.
|
||||
|
||||
var Cr func(...interface{}) string = cfunc(CatRed, color.FgRed)
|
||||
var Cg func(...interface{}) string = cfunc(CatGreen, color.FgGreen)
|
||||
var Cy func(...interface{}) string = cfunc(CatYellow, color.FgYellow)
|
||||
var Cb func(...interface{}) string = cfunc(CatBlue, color.FgBlue)
|
||||
var Cm func(...interface{}) string = cfunc(CatPink, color.FgMagenta)
|
||||
var Cc func(...interface{}) string = cfunc(CatTeal, color.FgCyan)
|
||||
var Cw func(...interface{}) string = cfunc(CatSubtext1, color.FgWhite)
|
||||
|
||||
var Crb func(...interface{}) string = cfunc(CatRed, color.FgRed, color.Bold)
|
||||
var Cgb func(...interface{}) string = cfunc(CatGreen, color.FgGreen, color.Bold)
|
||||
var Cyb func(...interface{}) string = cfunc(CatYellow, color.FgYellow, color.Bold)
|
||||
var Cbb func(...interface{}) string = cfunc(CatBlue, color.FgBlue, color.Bold)
|
||||
var Cmb func(...interface{}) string = cfunc(CatPink, color.FgMagenta, color.Bold)
|
||||
var Ccb func(...interface{}) string = cfunc(CatTeal, color.FgCyan, color.Bold)
|
||||
var Cwb func(...interface{}) string = cfunc(CatText, color.FgWhite, color.Bold) // text: the themes foreground
|
||||
|
||||
// ------------------------------------------------------------------------------------- survey prompt colors
|
||||
//
|
||||
// survey renders its prompts from templates that call {{color "green+hb"}} and friends, which normally
|
||||
// ends up in mgutz/ansi - and that only speaks 16/256 colors. replacing the template function is enough
|
||||
// to theme every prompt including its icons. survey computes its layout from a separate color free
|
||||
// rendering, so the longer truecolor sequences cannot disturb the cursor placement.
|
||||
|
||||
func init() { core.TemplateFuncsWithColor["color"] = catcolor } // must run before the first prompt is parsed
|
||||
|
||||
func catcolor(style string) string { // ------------------------- mgutz/ansi style string -> catppuccin mocha
|
||||
if style == "reset" {
|
||||
return "\033[0m"
|
||||
}
|
||||
if !truecolor() || strings.Contains(style, ":") { // background colors stay with the original mapping
|
||||
return ansi.ColorCode(style)
|
||||
}
|
||||
|
||||
name, attrs, _ := strings.Cut(style, "+")
|
||||
c, ok := catansi[name]
|
||||
if !ok {
|
||||
return ansi.ColorCode(style)
|
||||
}
|
||||
|
||||
seq := ""
|
||||
for _, a := range attrs { // 'h' (bright) is a no-op: mocha uses the same value for both
|
||||
switch a {
|
||||
case 'b':
|
||||
seq += "1;"
|
||||
case 'd':
|
||||
seq += "2;"
|
||||
case 'u':
|
||||
seq += "4;"
|
||||
case 'i':
|
||||
seq += "7;"
|
||||
}
|
||||
}
|
||||
return SF("\033[%s38;2;%d;%d;%dm", seq, c.r, c.g, c.b)
|
||||
}
|
||||
|
||||
// ========================================================================================================= END
|
||||
Reference in New Issue
Block a user