initial commit
This commit is contained in:
8
Rikuri.sublime-project
Normal file
8
Rikuri.sublime-project
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"path": ".",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
cli.go
Normal file
41
cli.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Command-line arguments parsing and formatted console output.
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Terminal capabilites.
|
||||||
|
type TerminalInfo struct {
|
||||||
|
IsTTY bool
|
||||||
|
SupportsColor bool
|
||||||
|
Width int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns cached TerminalInfo for os.Stdin.
|
||||||
|
var GetStdinInfo = sync.OnceValue(func() TerminalInfo {
|
||||||
|
return GetTerminalInfo(os.Stdin)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Returns cached TerminalInfo for os.Stdout.
|
||||||
|
var GetStdoutInfo = sync.OnceValue(func() TerminalInfo {
|
||||||
|
return GetTerminalInfo(os.Stdout)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Returns cached TerminalInfo for os.Stderr.
|
||||||
|
var GetStderrInfo = sync.OnceValue(func() TerminalInfo {
|
||||||
|
return GetTerminalInfo(os.Stderr)
|
||||||
|
})
|
||||||
|
|
||||||
|
type ExitReason uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserError ExitReason = 1
|
||||||
|
BadUsage = 2
|
||||||
|
InternalError = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShortCircuit() {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
227
cli_args.go
Normal file
227
cli_args.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A parsed command-line argument.
|
||||||
|
type Argument struct {
|
||||||
|
name string
|
||||||
|
suffix string
|
||||||
|
value string
|
||||||
|
isFlag bool
|
||||||
|
mayBeCommand bool
|
||||||
|
hasValue bool
|
||||||
|
wasUsed bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn an argument into an Argument struct.
|
||||||
|
func parseArgument(arg string, into Args, acceptFlags bool) Args {
|
||||||
|
return append(into, Argument{value: arg, mayBeCommand: acceptFlags})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a "long" flag such as --name.
|
||||||
|
func parseLongFlag(arg string, into Args) Args {
|
||||||
|
var (
|
||||||
|
flag = Argument{isFlag: true}
|
||||||
|
name string
|
||||||
|
hasSuffix bool
|
||||||
|
)
|
||||||
|
|
||||||
|
name, flag.value, flag.hasValue = strings.Cut(arg[2:], "=")
|
||||||
|
flag.name, flag.suffix, hasSuffix = strings.Cut(name, ":")
|
||||||
|
|
||||||
|
if len(flag.name) == 0 {
|
||||||
|
flag.err = fmt.Errorf("Flag \"%s\" has no name", arg)
|
||||||
|
}
|
||||||
|
if hasSuffix && len(flag.suffix) == 0 {
|
||||||
|
flag.err = fmt.Errorf("Flag \"%s\" has a colon in its name but no suffix", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(into, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a "short" flag such as -n.
|
||||||
|
// -name will be parsed as four boolean flags -n, -a, -m and -e.
|
||||||
|
func parseShortFlag(arg string, into Args) Args {
|
||||||
|
name, value, hasValue := strings.Cut(arg[1:], "=")
|
||||||
|
prefix, suffix, hasSuffix := strings.Cut(name, ":")
|
||||||
|
prefixes := strings.Split(prefix, "")
|
||||||
|
|
||||||
|
if len(prefixes) == 0 {
|
||||||
|
flag := Argument{
|
||||||
|
isFlag: true, name: prefix, suffix: suffix, hasValue: hasValue, value: value,
|
||||||
|
err: fmt.Errorf("Flag \"%s\" has no name", arg),
|
||||||
|
}
|
||||||
|
return append(into, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range prefixes {
|
||||||
|
flag := Argument{isFlag: true, name: p}
|
||||||
|
if i == len(prefixes)-1 {
|
||||||
|
flag.suffix = suffix
|
||||||
|
flag.hasValue = hasValue
|
||||||
|
flag.value = value
|
||||||
|
if hasSuffix && len(suffix) == 0 {
|
||||||
|
flag.err = fmt.Errorf("Flag group \"%s\" contains a colon but no suffix", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
into = append(into, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return into
|
||||||
|
}
|
||||||
|
|
||||||
|
type Args []Argument
|
||||||
|
|
||||||
|
// Parse all command-line arguments from os.Args.
|
||||||
|
func ParseArgs() (arglist Args) {
|
||||||
|
var acceptFlags bool = true
|
||||||
|
|
||||||
|
for i, arg := range os.Args {
|
||||||
|
if i == 0 {
|
||||||
|
// skip program name
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if acceptFlags && strings.HasPrefix(arg, "--") {
|
||||||
|
if len(arg) > 2 {
|
||||||
|
arglist = parseLongFlag(arg, arglist)
|
||||||
|
} else {
|
||||||
|
// everything after the first double dash "--" is an argument
|
||||||
|
acceptFlags = false
|
||||||
|
}
|
||||||
|
} else if acceptFlags && strings.HasPrefix(arg, "-") {
|
||||||
|
if len(arg) > 1 {
|
||||||
|
arglist = parseShortFlag(arg, arglist)
|
||||||
|
} else {
|
||||||
|
// a single dash "-" has no special meaning
|
||||||
|
arglist = parseArgument(arg, arglist, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arglist = parseArgument(arg, arglist, acceptFlags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arglist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print an Argument as a flag.
|
||||||
|
func sprintFlag(flag Argument) string {
|
||||||
|
if utf8.RuneCountInString(flag.name) < 2 {
|
||||||
|
return fmt.Sprintf("\"-%s\"", flag.name)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("\"--%s\"", flag.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print a flag given its long and short form.
|
||||||
|
func sprintFlagName(name string, shorthand string) string {
|
||||||
|
if shorthand == "" {
|
||||||
|
return fmt.Sprintf("\"--%s\"", name)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("\"-%s\" or \"--%s\"", shorthand, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the flag named [name] or [shorthand], or <nil> if none if found.
|
||||||
|
// Fails if more than one flag is found.
|
||||||
|
func (args *Args) findSingleFlag(name string, shorthand string) (*Argument, error) {
|
||||||
|
var (
|
||||||
|
found *Argument
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for i := range *args {
|
||||||
|
arg := &(*args)[i]
|
||||||
|
if !arg.wasUsed && arg.isFlag && (arg.name == name || arg.name == shorthand) {
|
||||||
|
if found == nil {
|
||||||
|
found = arg
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Flag %s can only be used once", sprintFlagName(name, shorthand))
|
||||||
|
}
|
||||||
|
arg.wasUsed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return wether an argument is considered a help flag. -h, --help and windows-style /? are accepted.
|
||||||
|
func (arg *Argument) isHelpFlag() bool {
|
||||||
|
if arg.isFlag {
|
||||||
|
return arg.name == "h" || arg.name == "help"
|
||||||
|
} else if arg.mayBeCommand {
|
||||||
|
return arg.value == "/?"
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return wether a help flag is present in [args].
|
||||||
|
func (args *Args) HelpFlag() bool {
|
||||||
|
var (
|
||||||
|
result bool = false
|
||||||
|
)
|
||||||
|
for _, arg := range *args {
|
||||||
|
if !arg.wasUsed && arg.isHelpFlag() {
|
||||||
|
arg.wasUsed = true
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a boolean flag. The return value will be [value] if the flag is set.
|
||||||
|
func (args *Args) BoolFlag(name string, shorthand string, value bool) (bool, error) {
|
||||||
|
var result bool = !value
|
||||||
|
flag, err := args.findSingleFlag(name, shorthand)
|
||||||
|
|
||||||
|
if flag != nil {
|
||||||
|
if flag.suffix != "" {
|
||||||
|
err = fmt.Errorf("Flag %s cannot have a suffix", sprintFlagName(name, shorthand))
|
||||||
|
} else if flag.hasValue {
|
||||||
|
err = fmt.Errorf("Flag %s cannot have a value", sprintFlagName(name, shorthand))
|
||||||
|
} else {
|
||||||
|
result = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the next argument as a command.
|
||||||
|
func (args *Args) Command() (cmd string) {
|
||||||
|
for i := range *args {
|
||||||
|
arg := &(*args)[i]
|
||||||
|
if !arg.wasUsed && arg.mayBeCommand {
|
||||||
|
cmd = arg.value
|
||||||
|
arg.wasUsed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that all arguments have been used.
|
||||||
|
func (args *Args) Done() {
|
||||||
|
var errs []error
|
||||||
|
for _, arg := range *args {
|
||||||
|
if !arg.wasUsed {
|
||||||
|
if arg.isFlag {
|
||||||
|
errs = append(errs, fmt.Errorf("Unexpected flag %s", sprintFlag(arg)))
|
||||||
|
} else {
|
||||||
|
errs = append(errs, fmt.Errorf("Unexpected argument \"%s\"", arg.value))
|
||||||
|
}
|
||||||
|
} else if arg.err != nil {
|
||||||
|
errs = append(errs, arg.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errs != nil {
|
||||||
|
InvalidArgs(errs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
cli_darwin.go
Normal file
60
cli_darwin.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
/* The code in this file comes from the "github.com/evanw/esbuild/internal/logger" package.
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Evan Wallace
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTerminalInfo(file *os.File) (info TerminalInfo) {
|
||||||
|
fd := file.Fd()
|
||||||
|
|
||||||
|
// Is this file descriptor a terminal?
|
||||||
|
if _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA); err == nil {
|
||||||
|
info.IsTTY = true
|
||||||
|
info.SupportsColor = true
|
||||||
|
|
||||||
|
// Get the width of the window
|
||||||
|
if w, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ); err == nil {
|
||||||
|
info.Width = int(w.Col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorStyle(f *os.File) {
|
||||||
|
f.WriteString("\x1b[91m")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetStyle(f *os.File) {
|
||||||
|
f.WriteString("\x1b[0m")
|
||||||
|
}
|
||||||
122
cli_display.go
Normal file
122
cli_display.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Printer struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
Program string
|
||||||
|
Debugging bool
|
||||||
|
Color bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (printer *Printer) Debug(v ...any) {
|
||||||
|
if printer.Debugging {
|
||||||
|
printer.mutex.Lock()
|
||||||
|
fmt.Fprint(os.Stdout, "dbg ")
|
||||||
|
_, file, line, ok := runtime.Caller(2)
|
||||||
|
if ok {
|
||||||
|
lastSegment := strings.LastIndex(file, "/") + 1
|
||||||
|
fmt.Fprint(os.Stdout, "@ ", file[lastSegment:], ":", line, " ")
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, v...)
|
||||||
|
printer.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fprintIndented(f *os.File, maxWidth int, indent int, value any) {
|
||||||
|
wrapped := wrap.Indents(indent, wrap.Wrap(fmt.Sprint(value), maxWidth-indent))
|
||||||
|
for _, line := range wrapped {
|
||||||
|
fmt.Fprintln(f, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (printer *Printer) printErrorMessage(msg string, details ...error) {
|
||||||
|
termInfo := GetStderrInfo()
|
||||||
|
if termInfo.SupportsColor && printer.Color {
|
||||||
|
errorStyle(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stderr, "Error:", msg)
|
||||||
|
for _, detail := range details {
|
||||||
|
fprintIndented(os.Stderr, termInfo.Width, 3, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if termInfo.SupportsColor && printer.Color {
|
||||||
|
resetStyle(os.Stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (printer *Printer) Error(msg string, details ...error) {
|
||||||
|
printer.mutex.Lock()
|
||||||
|
printer.printErrorMessage(msg, details...)
|
||||||
|
printer.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (printer *Printer) Fatal(msg string, reason ExitReason, details ...error) {
|
||||||
|
printer.mutex.Lock()
|
||||||
|
printer.printErrorMessage(msg, details...)
|
||||||
|
if reason == BadUsage {
|
||||||
|
fmt.Fprintf(os.Stdout, "run '%s --help' for usage\n", printer.Program)
|
||||||
|
}
|
||||||
|
os.Exit(int(reason))
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPrinter Printer
|
||||||
|
|
||||||
|
func DefaultPrinter() *Printer {
|
||||||
|
return &defaultPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(v ...any) {
|
||||||
|
defaultPrinter.Debug(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(msg string, details ...error) {
|
||||||
|
defaultPrinter.Error(msg, details...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatal(msg string, reason ExitReason, details ...error) {
|
||||||
|
defaultPrinter.Fatal(msg, reason, details...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InvalidArgs(errs ...error) {
|
||||||
|
Fatal("invalid command-line arguments", BadUsage, errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Show(msg string) {
|
||||||
|
termInfo := GetStdoutInfo()
|
||||||
|
wrapped := wrap.Wrap(msg, termInfo.Width)
|
||||||
|
for _, line := range wrapped {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowUsage(explanation string, examples ...string) {
|
||||||
|
fmt.Print(explanation, "\n\nUsage:\n")
|
||||||
|
for _, line := range wrap.Indents(3, examples) {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DescribeOption(name string, description string) {
|
||||||
|
name = " " + name + " "
|
||||||
|
termInfo := GetStdoutInfo()
|
||||||
|
wrapped := wrap.Indentfs(name, len(name), wrap.Wrap(description, termInfo.Width-len(name)))
|
||||||
|
for _, line := range wrapped {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AskString(prompt string) string {
|
||||||
|
fmt.Print(prompt)
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
text, _ := reader.ReadString('\n')
|
||||||
|
return text
|
||||||
|
}
|
||||||
62
cli_linux.go
Normal file
62
cli_linux.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
/* The code in this file comes from the "github.com/evanw/esbuild/internal/logger" package.
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Evan Wallace
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SupportsColorEscapes = true
|
||||||
|
|
||||||
|
func GetTerminalInfo(file *os.File) (info TerminalInfo) {
|
||||||
|
fd := file.Fd()
|
||||||
|
|
||||||
|
// Is this file descriptor a terminal?
|
||||||
|
if _, err := unix.IoctlGetTermios(int(fd), unix.TCGETS); err == nil {
|
||||||
|
info.IsTTY = true
|
||||||
|
info.SupportsColor = true
|
||||||
|
|
||||||
|
// Get the width of the window
|
||||||
|
if w, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ); err == nil {
|
||||||
|
info.Width = int(w.Col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorStyle(f *os.File) {
|
||||||
|
f.WriteString("\x1b[91m")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetStyle(f *os.File) {
|
||||||
|
f.WriteString("\x1b[0m")
|
||||||
|
}
|
||||||
14
cli_other.go
Normal file
14
cli_other.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build !darwin && !linux && !windows
|
||||||
|
// +build !darwin,!linux,!windows
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func GetTerminalInfo(*os.File) TerminalInfo {
|
||||||
|
return TerminalInfo{Width: 80}
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorStyle(*os.File) { }
|
||||||
|
|
||||||
|
func resetStyle(*os.File) { }
|
||||||
76
cli_windows.go
Normal file
76
cli_windows.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
/* The code in this file comes from the "github.com/evanw/esbuild/internal/logger" package.
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Evan Wallace
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SupportsColorEscapes = true
|
||||||
|
|
||||||
|
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
var getConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||||
|
var getConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||||
|
|
||||||
|
type consoleScreenBufferInfo struct {
|
||||||
|
dwSizeX int16
|
||||||
|
dwSizeY int16
|
||||||
|
dwCursorPositionX int16
|
||||||
|
dwCursorPositionY int16
|
||||||
|
wAttributes uint16
|
||||||
|
srWindowLeft int16
|
||||||
|
srWindowTop int16
|
||||||
|
srWindowRight int16
|
||||||
|
srWindowBottom int16
|
||||||
|
dwMaximumWindowSizeX int16
|
||||||
|
dwMaximumWindowSizeY int16
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTerminalInfo(file *os.File) TerminalInfo {
|
||||||
|
fd := file.Fd()
|
||||||
|
|
||||||
|
// Is this file descriptor a terminal?
|
||||||
|
var unused uint32
|
||||||
|
isTTY, _, _ := syscall.SyscallN(getConsoleMode.Addr(), fd, uintptr(unsafe.Pointer(&unused)))
|
||||||
|
|
||||||
|
// Get the width of the window
|
||||||
|
var info consoleScreenBufferInfo
|
||||||
|
syscall.SyscallN(getConsoleScreenBufferInfo.Addr(), fd, uintptr(unsafe.Pointer(&info)))
|
||||||
|
|
||||||
|
return TerminalInfo{
|
||||||
|
IsTTY: isTTY != 0,
|
||||||
|
Width: int(info.dwSizeX) - 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorStyle(*os.File) { }
|
||||||
|
|
||||||
|
func resetStyle(*os.File) { }
|
||||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module louisdevie.fr/rikuri
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/sys v0.43.0
|
||||||
|
)
|
||||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
github.com/louisdevie/elizalina2 v0.0.0-20251210181249-159b3a136b35 h1:xtJtD7jpUAL/ZUupW85Y5rfpjtgEVtzVC8Q+rhgvrQs=
|
||||||
|
github.com/louisdevie/elizalina2 v0.0.0-20251210181249-159b3a136b35/go.mod h1:uajSlR/gzlBN5iOPWySVFsXNhWYkVatEzQ7HFR9j46Q=
|
||||||
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
126
wrap.go
Normal file
126
wrap.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package wrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type measuredString struct {
|
||||||
|
text string
|
||||||
|
unicodeSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (str *measuredString) pushRune(text string, size int) string {
|
||||||
|
str.text += text[:size]
|
||||||
|
str.unicodeSize++
|
||||||
|
return text[size:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func concat(a, b measuredString) measuredString {
|
||||||
|
return measuredString{
|
||||||
|
text: a.text + b.text,
|
||||||
|
unicodeSize: a.unicodeSize + b.unicodeSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given the space remaining on the current line as minWidth and the maximum line size as maxWidth,
|
||||||
|
// this function will read the first word and return three values:
|
||||||
|
// 1) the text that should go at the end of the current line,
|
||||||
|
// 2) the text that should go on the next line,
|
||||||
|
// 3) the remaining text that has not been consumed.
|
||||||
|
func wrapWord(text string, minWidth int, maxWidth int) (measuredString, measuredString, string) {
|
||||||
|
var space, prefix, suffix measuredString
|
||||||
|
reachedMin := false
|
||||||
|
reachedEnd := false
|
||||||
|
// read the first word, stopping at the end of the current line
|
||||||
|
for !reachedMin && len(text) > 0 {
|
||||||
|
rune, size := utf8.DecodeRuneInString(text)
|
||||||
|
if unicode.IsSpace(rune) {
|
||||||
|
if prefix.unicodeSize == 0 {
|
||||||
|
text = space.pushRune(text, size)
|
||||||
|
} else {
|
||||||
|
reachedMin = true
|
||||||
|
reachedEnd = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if space.unicodeSize+prefix.unicodeSize < minWidth {
|
||||||
|
text = prefix.pushRune(text, size)
|
||||||
|
} else {
|
||||||
|
reachedMin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// does the entire word fit on the current line ?
|
||||||
|
if reachedEnd || len(text) == 0 {
|
||||||
|
return concat(space, prefix), measuredString{}, text
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise finish reading the first word
|
||||||
|
for !reachedEnd && len(text) > 0 {
|
||||||
|
rune, size := utf8.DecodeRuneInString(text)
|
||||||
|
if unicode.IsSpace(rune) {
|
||||||
|
reachedEnd = true
|
||||||
|
} else {
|
||||||
|
if prefix.unicodeSize+suffix.unicodeSize < maxWidth {
|
||||||
|
text = suffix.pushRune(text, size)
|
||||||
|
} else {
|
||||||
|
// special case: if the word cannot fit on a single line,
|
||||||
|
// we put half the word on the current line and the other half on the next line
|
||||||
|
return concat(space, prefix), suffix, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// and put the word on the next line
|
||||||
|
return measuredString{}, concat(prefix, suffix), text
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break a string into lines of at most [width] runes.
|
||||||
|
func Wrap(text string, width int) (result []string) {
|
||||||
|
var line measuredString
|
||||||
|
for len(text) > 0 {
|
||||||
|
var cont, next measuredString
|
||||||
|
cont, next, text = wrapWord(text, width-line.unicodeSize, width)
|
||||||
|
|
||||||
|
line = concat(line, cont)
|
||||||
|
if next.unicodeSize > 0 {
|
||||||
|
result = append(result, line.text)
|
||||||
|
line = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add any text remaining in the buffer
|
||||||
|
return append(result, line.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix multiple lines of text with a string.
|
||||||
|
func Indent(prefix string, lines []string) []string {
|
||||||
|
result := make([]string, len(lines))
|
||||||
|
for i, line := range lines {
|
||||||
|
result[i] = prefix + line
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix multiple lines of text using a different string for the first line.
|
||||||
|
func Indentf(first string, prefix string, lines []string) []string {
|
||||||
|
result := make([]string, len(lines))
|
||||||
|
for i, line := range lines {
|
||||||
|
if i == 0 {
|
||||||
|
result[i] = first + line
|
||||||
|
} else {
|
||||||
|
result[i] = prefix + line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix multiple lines of text with a number of spaces.
|
||||||
|
func Indents(size int, lines []string) []string {
|
||||||
|
return Indent(strings.Repeat(" ", size), lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix multiple lines of text with a number of spaces,
|
||||||
|
// but using a different string for the first line.
|
||||||
|
func Indentfs(first string, size int, lines []string) []string {
|
||||||
|
return Indentf(first, strings.Repeat(" ", size), lines)
|
||||||
|
}
|
||||||
74
wrap_test.go
Normal file
74
wrap_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package wrap_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/louisdevie/elizalina2/internal/wrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assertSameLines(t *testing.T, wrapped []string, expected []string) {
|
||||||
|
if len(wrapped) != len(expected) {
|
||||||
|
t.Fatalf("Expected %d lines of text but got %d", len(expected), len(wrapped))
|
||||||
|
}
|
||||||
|
for i, e := range expected {
|
||||||
|
if wrapped[i] != e {
|
||||||
|
t.Logf("Expected line %d to be \"%s\"", i+1, e)
|
||||||
|
t.Logf(" but got \"%s\"", wrapped[i])
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = "Or was it because of the involvement of something from beyond science?"
|
||||||
|
|
||||||
|
func TestWrapWide(t *testing.T) {
|
||||||
|
wrapped := wrap.Wrap(text, 80)
|
||||||
|
expected := []string{"Or was it because of the involvement of something from beyond science?"}
|
||||||
|
assertSameLines(t, wrapped, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapMedium(t *testing.T) {
|
||||||
|
wrapped := wrap.Wrap(text, 44)
|
||||||
|
expected := []string{"Or was it because of the involvement of", "something from beyond science?"}
|
||||||
|
assertSameLines(t, wrapped, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapNarrow(t *testing.T) {
|
||||||
|
wrapped := wrap.Wrap(text, 10)
|
||||||
|
expected := []string{"Or was it", "because of", "the involv", "ement of", "something", "from", "beyond", "science?"}
|
||||||
|
assertSameLines(t, wrapped, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapWithoutSpace(t *testing.T) {
|
||||||
|
wrapped := wrap.Wrap("Orwasitbecauseoftheinvolvementofsomethingfrombeyondscience?", 10)
|
||||||
|
expected := []string{"Orwasitbec", "auseofthei", "nvolvement", "ofsomethin", "gfrombeyon", "dscience?"}
|
||||||
|
assertSameLines(t, wrapped, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndent(t *testing.T) {
|
||||||
|
lines := []string{"Or was it because of the", "involvement of something", "from beyond science?"}
|
||||||
|
indented := wrap.Indent("| ", lines)
|
||||||
|
expected := []string{"| Or was it because of the", "| involvement of something", "| from beyond science?"}
|
||||||
|
assertSameLines(t, indented, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndentf(t *testing.T) {
|
||||||
|
lines := []string{"Or was it because of the", "involvement of something", "from beyond science?"}
|
||||||
|
indented := wrap.Indentf("-> ", "| ", lines)
|
||||||
|
expected := []string{"-> Or was it because of the", "| involvement of something", "| from beyond science?"}
|
||||||
|
assertSameLines(t, indented, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndents(t *testing.T) {
|
||||||
|
lines := []string{"Or was it because of the", "involvement of something", "from beyond science?"}
|
||||||
|
indented := wrap.Indents(3, lines)
|
||||||
|
expected := []string{" Or was it because of the", " involvement of something", " from beyond science?"}
|
||||||
|
assertSameLines(t, indented, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndentfs(t *testing.T) {
|
||||||
|
lines := []string{"Or was it because of the", "involvement of something", "from beyond science?"}
|
||||||
|
indented := wrap.Indentfs("-> ", 3, lines)
|
||||||
|
expected := []string{"-> Or was it because of the", " involvement of something", " from beyond science?"}
|
||||||
|
assertSameLines(t, indented, expected)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user