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