Initial QSfera import
This commit is contained in:
+197
@@ -0,0 +1,197 @@
|
||||
package termenv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ColorLevel is the color level supported by a terminal.
|
||||
type ColorLevel uint8
|
||||
|
||||
const (
|
||||
TermColorNone ColorLevel = iota // not support color
|
||||
TermColor16 // 16(4bit) ANSI color supported
|
||||
TermColor256 // 256(8bit) color supported
|
||||
TermColorTrue // support TRUE(RGB) color
|
||||
)
|
||||
|
||||
// String returns the string name of the color level.
|
||||
func (l ColorLevel) String() string {
|
||||
switch l {
|
||||
case TermColor16:
|
||||
return "ansiColor"
|
||||
case TermColor256:
|
||||
return "256color"
|
||||
case TermColorTrue:
|
||||
return "trueColor"
|
||||
default:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
// NoColor returns true if the NO_COLOR environment variable is set.
|
||||
func NoColor() bool { return noColor }
|
||||
|
||||
// TermColorLevel returns the color support level for the current terminal.
|
||||
func TermColorLevel() ColorLevel { return colorLevel }
|
||||
|
||||
// IsSupportColor returns true if the terminal supports color.
|
||||
func IsSupportColor() bool { return colorLevel > TermColorNone }
|
||||
|
||||
// IsSupport256Color returns true if the terminal supports 256 colors.
|
||||
func IsSupport256Color() bool { return colorLevel >= TermColor256 }
|
||||
|
||||
// IsSupportTrueColor returns true if the terminal supports true color.
|
||||
func IsSupportTrueColor() bool { return colorLevel == TermColorTrue }
|
||||
|
||||
//
|
||||
// ---------------- Force set color support ----------------
|
||||
//
|
||||
|
||||
var backLevel ColorLevel
|
||||
|
||||
// SetColorLevel value force.
|
||||
func SetColorLevel(level ColorLevel) {
|
||||
// backup old value
|
||||
backLevel = colorLevel
|
||||
|
||||
// force set color level
|
||||
colorLevel = level
|
||||
supportColor = level > TermColorNone
|
||||
noColor = supportColor == false
|
||||
}
|
||||
|
||||
// DisableColor in the current terminal
|
||||
func DisableColor() {
|
||||
// backup old value
|
||||
backLevel = colorLevel
|
||||
|
||||
// force disable color
|
||||
noColor = true
|
||||
supportColor = false
|
||||
colorLevel = TermColorNone
|
||||
}
|
||||
|
||||
// ForceEnableColor flags value. TIP: use for unit testing.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ccolor.ForceEnableColor()
|
||||
// defer ccolor.RevertColorSupport()
|
||||
func ForceEnableColor() {
|
||||
// backup old value
|
||||
backLevel = colorLevel
|
||||
|
||||
// force enables color
|
||||
noColor = false
|
||||
supportColor = true
|
||||
colorLevel = TermColor256
|
||||
// return colorLevel
|
||||
}
|
||||
|
||||
// RevertColorSupport flags to init value.
|
||||
func RevertColorSupport() {
|
||||
// revert color flags var
|
||||
colorLevel = backLevel
|
||||
supportColor = backLevel > TermColorNone
|
||||
noColor = os.Getenv("NO_COLOR") == ""
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* helper methods for detect color supports
|
||||
*************************************************************/
|
||||
|
||||
// DetectColorLevel for current env
|
||||
//
|
||||
// NOTICE: The method will detect terminal info each time.
|
||||
//
|
||||
// if only want to get current color level, please direct call IsSupportColor() or TermColorLevel()
|
||||
func DetectColorLevel() ColorLevel {
|
||||
level, _ := detectTermColorLevel()
|
||||
return level
|
||||
}
|
||||
|
||||
// on TERM=screen: not support true-color
|
||||
const noTrueColorTerm = "screen"
|
||||
|
||||
// detect terminal color support level
|
||||
//
|
||||
// refer https://github.com/Delta456/box-cli-maker
|
||||
func detectTermColorLevel() (level ColorLevel, needVTP bool) {
|
||||
isWin := runtime.GOOS == "windows"
|
||||
termVal := os.Getenv("TERM")
|
||||
|
||||
if termVal != noTrueColorTerm {
|
||||
// On JetBrains Terminal
|
||||
// - TERM value not set, but support true-color
|
||||
// env:
|
||||
// TERMINAL_EMULATOR=JetBrains-JediTerm
|
||||
val := os.Getenv("TERMINAL_EMULATOR")
|
||||
if val == "JetBrains-JediTerm" {
|
||||
debugf("True Color support on JetBrains-JediTerm, is win: %v", isWin)
|
||||
return TermColorTrue, false
|
||||
}
|
||||
}
|
||||
|
||||
level = detectColorLevelFromEnv(termVal, isWin)
|
||||
|
||||
// fallback: simple detect by TERM value string.
|
||||
if level == TermColorNone {
|
||||
debugf("level=none - fallback check special term color support")
|
||||
level, needVTP = detectSpecialTermColor(termVal)
|
||||
debugf("color level by detectSpecialTermColor: %s", level.String())
|
||||
} else {
|
||||
debugf("color level by detectColorLevelFromEnv: %s", level.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// detectColorFromEnv returns the color level COLORTERM, FORCE_COLOR,
|
||||
// TERM_PROGRAM, or determined from the TERM environment variable.
|
||||
//
|
||||
// refer the github.com/xo/terminfo.ColorLevelFromEnv()
|
||||
// https://en.wikipedia.org/wiki/Terminfo
|
||||
func detectColorLevelFromEnv(termVal string, isWin bool) ColorLevel {
|
||||
if termVal == noTrueColorTerm { // on TERM=screen: not support true-color
|
||||
return TermColor256
|
||||
}
|
||||
|
||||
// check for overriding environment variables
|
||||
colorTerm, termProg, forceColor := os.Getenv("COLORTERM"), os.Getenv("TERM_PROGRAM"), os.Getenv("FORCE_COLOR")
|
||||
switch {
|
||||
case strings.Contains(colorTerm, "truecolor") || strings.Contains(colorTerm, "24bit"):
|
||||
return TermColorTrue
|
||||
case colorTerm != "" || forceColor != "":
|
||||
return TermColor16
|
||||
case termProg == "Apple_Terminal":
|
||||
return TermColor256
|
||||
case termProg == "Terminus" || termProg == "Hyper":
|
||||
return TermColorTrue
|
||||
case termProg == "iTerm.app":
|
||||
// check iTerm version
|
||||
termVer := os.Getenv("TERM_PROGRAM_VERSION")
|
||||
if termVer != "" {
|
||||
i, err := strconv.Atoi(strings.Split(termVer, ".")[0])
|
||||
if err != nil {
|
||||
setLastErr(errors.New("invalid TERM_PROGRAM_VERSION=" + termVer))
|
||||
return TermColor256 // return TermColorNone
|
||||
}
|
||||
if i == 3 {
|
||||
return TermColorTrue
|
||||
}
|
||||
}
|
||||
return TermColor256
|
||||
}
|
||||
|
||||
// otherwise determine from TERM's max_colors capability
|
||||
// if !isWin && termVal != "" {
|
||||
// debugf("TERM=%s - TODO check color level by load terminfo file", termVal)
|
||||
// return TermColor16
|
||||
// }
|
||||
|
||||
// no TERM env value. default return none level
|
||||
return TermColorNone
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
//go:build !windows
|
||||
|
||||
package termenv
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gookit/goutil/internal/checkfn"
|
||||
)
|
||||
|
||||
// detect special term color support on macOS, linux, unix
|
||||
func detectSpecialTermColor(termVal string) (ColorLevel, bool) {
|
||||
if termVal == "" {
|
||||
// detect WSL as it has True Color support
|
||||
// on Windows WSL:
|
||||
// - runtime.GOOS == "Linux"
|
||||
// - support true-color
|
||||
if checkfn.IsWSL() {
|
||||
debugf("True Color support on WSL environment")
|
||||
return TermColorTrue, false
|
||||
}
|
||||
return TermColorNone, false
|
||||
}
|
||||
|
||||
debugf("terminfo check - fallback detect color by check TERM value")
|
||||
|
||||
// on TERM=screen:
|
||||
// - support 256, not support true-color. test on macOS
|
||||
if termVal == noTrueColorTerm {
|
||||
return TermColor256, false
|
||||
}
|
||||
|
||||
if strings.Contains(termVal, "256color") {
|
||||
return TermColor256, false
|
||||
}
|
||||
|
||||
if strings.Contains(termVal, "xterm") {
|
||||
return TermColor256, false
|
||||
}
|
||||
return TermColor16, false
|
||||
}
|
||||
|
||||
func syscallStdinFd() int { return syscall.Stdin }
|
||||
|
||||
func syscallStdoutFd() int { return syscall.Stdout }
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
//go:build windows
|
||||
|
||||
package termenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Get the Windows Version and Build Number
|
||||
var majorVersion, _, buildNumber = windows.RtlGetNtVersionNumbers()
|
||||
|
||||
// refer
|
||||
//
|
||||
// https://github.com/Delta456/box-cli-maker/blob/7b5a1ad8a016ce181e7d8b05e24b54ff60b4b38a/detect_windows.go#L30-L57
|
||||
// https://github.com/gookit/color/issues/25#issuecomment-738727917
|
||||
//
|
||||
// detects the color level supported on Windows: CMD, PowerShell
|
||||
func detectSpecialTermColor(_ string) (tl ColorLevel, needVTP bool) {
|
||||
if os.Getenv("ConEmuANSI") == "ON" {
|
||||
debugf("True Color support by ConEmuANSI=ON")
|
||||
// ConEmuANSI is "ON" for generic ANSI support
|
||||
// but True Color option is enabled by default
|
||||
// I am just assuming that people wouldn't have disabled it
|
||||
// Even if it is not enabled then ConEmu will auto round off accordingly
|
||||
return TermColorTrue, false
|
||||
}
|
||||
|
||||
// Before Windows 10 Build Number 10586, console never supported ANSI Colors
|
||||
if buildNumber < 10586 || majorVersion < 10 {
|
||||
// Detect if using ANSICON on older systems
|
||||
if os.Getenv("ANSICON") != "" {
|
||||
conVersion := os.Getenv("ANSICON_VER")
|
||||
// 8-bit Colors were only supported after v1.81 release
|
||||
if conVersion >= "181" {
|
||||
return TermColor256, false
|
||||
}
|
||||
return TermColor16, false
|
||||
}
|
||||
|
||||
return TermColorNone, false
|
||||
}
|
||||
|
||||
// True Color is not available before build 14931 so fallback to 8-bit color.
|
||||
if buildNumber < 14931 {
|
||||
return TermColor256, true
|
||||
}
|
||||
|
||||
// Windows 10 build 14931 is the first release that supports 16m/TrueColor
|
||||
debugf("support True Color on windows version is >= build 14931")
|
||||
return TermColorTrue, true
|
||||
}
|
||||
|
||||
// TryEnableVTP try force enables colors on Windows terminal
|
||||
func TryEnableVTP(enable bool) bool {
|
||||
if !enable {
|
||||
return false
|
||||
}
|
||||
|
||||
// enable colors on Windows terminal
|
||||
if tryEnableOnCONOUT() {
|
||||
debugf("True-Color by enable VirtualTerminalProcessing on windows")
|
||||
return true
|
||||
}
|
||||
|
||||
// initKernel32Proc()
|
||||
suc := tryEnableOnStdout()
|
||||
debugf("True-Color by enable VirtualTerminalProcessing on windows")
|
||||
return suc
|
||||
}
|
||||
|
||||
func tryEnableOnCONOUT() bool {
|
||||
outHandle, err := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
setLastErr(err)
|
||||
return false
|
||||
}
|
||||
|
||||
err = EnableVTProcessing(outHandle, true)
|
||||
if err != nil {
|
||||
setLastErr(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func tryEnableOnStdout() bool {
|
||||
// try direct open syscall.Stdout
|
||||
err := EnableVTProcessing(syscall.Stdout, true)
|
||||
if err != nil {
|
||||
setLastErr(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// related docs
|
||||
// https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences
|
||||
// https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences#samples
|
||||
var (
|
||||
// isMSys bool
|
||||
kernel32 *syscall.LazyDLL
|
||||
|
||||
procGetConsoleMode *syscall.LazyProc
|
||||
procSetConsoleMode *syscall.LazyProc
|
||||
)
|
||||
|
||||
func initKernel32Proc() {
|
||||
if kernel32 != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// load related Windows dll
|
||||
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* render full color code on Windows(8,16,24bit color)
|
||||
*************************************************************/
|
||||
|
||||
// EnableVTProcessing Enable virtual terminal processing on Windows
|
||||
//
|
||||
// ref from github.com/konsorten/go-windows-terminal-sequences
|
||||
// doc https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences#samples
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// err := EnableVTProcessing(syscall.Stdout, true)
|
||||
// // support print color text
|
||||
// err = EnableVTProcessing(syscall.Stdout, false)
|
||||
func EnableVTProcessing(stream syscall.Handle, enable bool) error {
|
||||
var mode uint32
|
||||
// Check if it is currently in the terminal
|
||||
// err := syscall.GetConsoleMode(syscall.Stdout, &mode)
|
||||
err := syscall.GetConsoleMode(stream, &mode)
|
||||
if err != nil {
|
||||
debugf("enable Windows VirtualTerminalProcessing error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// docs https://docs.microsoft.com/zh-cn/windows/console/getconsolemode#parameters
|
||||
if enable {
|
||||
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
} else {
|
||||
mode &^= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
}
|
||||
|
||||
err = windows.SetConsoleMode(windows.Handle(stream), mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ret, _, err := procSetConsoleMode.Call(uintptr(stream), uintptr(mode))
|
||||
// if ret == 0 {
|
||||
// return err
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
// on Windows, must convert 'syscall.Stdin' to int
|
||||
func syscallStdinFd() int {
|
||||
return int(syscall.Stdin)
|
||||
}
|
||||
|
||||
// on Windows, must convert to int
|
||||
func syscallStdoutFd() int {
|
||||
return int(syscall.Stdout)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package termenv
|
||||
|
||||
func init() {
|
||||
// terminal supports color OR noColor=true: Don't need to enable virtual process
|
||||
if colorLevel != TermColorNone || noColor {
|
||||
return
|
||||
}
|
||||
|
||||
// try force enable colors on Windows terminal
|
||||
TryEnableVTP(needVTP)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
// Package termenv provides detect color support of the current terminal.
|
||||
// And with some utils for terminal env.
|
||||
package termenv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
lastErr error
|
||||
// debug mode
|
||||
debugMode bool
|
||||
// support color of current terminal
|
||||
supportColor bool
|
||||
|
||||
// value of os color render and display
|
||||
//
|
||||
// NOTICE:
|
||||
// if ENV: NO_COLOR is not empty, will disable color render.
|
||||
noColor = os.Getenv("NO_COLOR") != ""
|
||||
|
||||
// the color support level for current terminal
|
||||
// needVTP - need enable VTP, only for Windows OS
|
||||
colorLevel, needVTP = detectTermColorLevel()
|
||||
)
|
||||
|
||||
// SetDebugMode sets debug mode.
|
||||
func SetDebugMode(enable bool) { debugMode = enable }
|
||||
|
||||
// LastErr returns the last error.
|
||||
func LastErr() error {
|
||||
defer func() {
|
||||
lastErr = nil // reset on get
|
||||
}()
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func debugf(tpl string, v ...any) {
|
||||
if debugMode {
|
||||
fmt.Printf("TERMENV: "+tpl+"\n", v...)
|
||||
}
|
||||
}
|
||||
|
||||
func setLastErr(err error) {
|
||||
if err != nil {
|
||||
debugf("TERMENV: last error: %v", err)
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
// exec: `stty -a 2>&1`
|
||||
// const (
|
||||
// mac: speed 9600 baud; 97 rows; 362 columns;
|
||||
// macSttyMsgPattern = `(\d+)\s+rows;\s*(\d+)\s+columns;`
|
||||
// linux: speed 38400 baud; rows 97; columns 362; line = 0;
|
||||
// linuxSttyMsgPattern = `rows\s+(\d+);\s*columns\s+(\d+);`
|
||||
// )
|
||||
var terminalWidth, terminalHeight int
|
||||
|
||||
// GetTermSize for current console terminal. will first try to get from environment variables COLUMNS and LINES.
|
||||
func GetTermSize(refresh ...bool) (w int, h int) {
|
||||
if terminalWidth > 0 && (len(refresh) == 0 || !refresh[0]) {
|
||||
return terminalWidth, terminalHeight
|
||||
}
|
||||
|
||||
// 首先尝试从环境变量获取
|
||||
if cols := os.Getenv("COLUMNS"); cols != "" {
|
||||
if width, err := strconv.Atoi(cols); err == nil && width > 0 {
|
||||
terminalWidth = width
|
||||
}
|
||||
}
|
||||
if rows := os.Getenv("LINES"); rows != "" {
|
||||
if height, err := strconv.Atoi(rows); err == nil && height > 0 {
|
||||
terminalHeight = height
|
||||
}
|
||||
}
|
||||
if terminalWidth > 0 && terminalHeight > 0 {
|
||||
return terminalWidth, terminalHeight
|
||||
}
|
||||
|
||||
var err error
|
||||
w, h, err = term.GetSize(syscallStdoutFd())
|
||||
if err != nil {
|
||||
debugf("get terminal size error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// cache result
|
||||
terminalWidth, terminalHeight = w, h
|
||||
debugf("get terminal size: %d,%d", w, h)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadPassword from console terminal
|
||||
func ReadPassword(question ...string) string {
|
||||
if len(question) > 0 {
|
||||
print(question[0])
|
||||
} else {
|
||||
print("Enter Password: ")
|
||||
}
|
||||
|
||||
bs, err := term.ReadPassword(syscallStdinFd())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
println() // new line
|
||||
return string(bs)
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package termenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/goutil/internal/checkfn"
|
||||
"github.com/gookit/goutil/internal/comfunc"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
cmdList = []string{"cmd", "cmd.exe"}
|
||||
pwshList = []string{"powershell", "powershell.exe", "pwsh", "pwsh.exe"}
|
||||
)
|
||||
|
||||
// IsTerminal 检查是否为终端设备中
|
||||
func IsTerminal() bool {
|
||||
fd := int(os.Stdout.Fd())
|
||||
return term.IsTerminal(fd)
|
||||
}
|
||||
|
||||
// CurrentShell get current used shell env file.
|
||||
//
|
||||
// eg "/bin/zsh" "/bin/bash".
|
||||
// if onlyName=true, will return "zsh", "bash"
|
||||
func CurrentShell(onlyName bool, fallbackShell ...string) string {
|
||||
return comfunc.CurrentShell(onlyName, fallbackShell...)
|
||||
}
|
||||
|
||||
// HasShellEnv has shell env check.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// HasShellEnv("sh")
|
||||
// HasShellEnv("bash")
|
||||
func HasShellEnv(shell string) bool {
|
||||
// can also use: "echo $0"
|
||||
out, err := shellExec("echo OK", shell)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(out) == "OK"
|
||||
}
|
||||
|
||||
// IsShellSpecialVar reports whether the character identifies a special
|
||||
// shell variable such as $*.
|
||||
func IsShellSpecialVar(c uint8) bool {
|
||||
switch c {
|
||||
case '*', '#', '$', '@', '!', '?', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shellExec(expr, shell string) (string, error) {
|
||||
// "-c" for bash,sh,zsh shell
|
||||
mark := "-c"
|
||||
if shell == "" {
|
||||
shell = CurrentShell(true, "sh")
|
||||
}
|
||||
|
||||
// special for Windows shell
|
||||
if runtime.GOOS == "windows" {
|
||||
// use cmd.exe, mark is "/c"
|
||||
if checkfn.StringsContains(cmdList, shell) {
|
||||
mark = "/c"
|
||||
} else if checkfn.StringsContains(pwshList, shell) {
|
||||
// "-Command" for powershell
|
||||
mark = "-Command"
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(shell, mark, expr)
|
||||
bs, err := cmd.CombinedOutput()
|
||||
return string(bs), err
|
||||
}
|
||||
Reference in New Issue
Block a user