From a5c79fb7b574d6743a8887f5313c679833d69864 Mon Sep 17 00:00:00 2001 From: Louis DEVIE Date: Mon, 20 Apr 2026 21:38:22 +0200 Subject: [PATCH] initial commit --- Rikuri.sublime-project | 8 ++ cli.go | 41 ++++++++ cli_args.go | 227 +++++++++++++++++++++++++++++++++++++++++ cli_darwin.go | 60 +++++++++++ cli_display.go | 122 ++++++++++++++++++++++ cli_linux.go | 62 +++++++++++ cli_other.go | 14 +++ cli_windows.go | 76 ++++++++++++++ go.mod | 7 ++ go.sum | 4 + wrap.go | 126 +++++++++++++++++++++++ wrap_test.go | 74 ++++++++++++++ 12 files changed, 821 insertions(+) create mode 100644 Rikuri.sublime-project create mode 100644 cli.go create mode 100644 cli_args.go create mode 100644 cli_darwin.go create mode 100644 cli_display.go create mode 100644 cli_linux.go create mode 100644 cli_other.go create mode 100644 cli_windows.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 wrap.go create mode 100644 wrap_test.go diff --git a/Rikuri.sublime-project b/Rikuri.sublime-project new file mode 100644 index 0000000..e4e0b6e --- /dev/null +++ b/Rikuri.sublime-project @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "name": "", + "path": ".", + } + ] +} \ No newline at end of file diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..4b16769 --- /dev/null +++ b/cli.go @@ -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) +} \ No newline at end of file diff --git a/cli_args.go b/cli_args.go new file mode 100644 index 0000000..702311b --- /dev/null +++ b/cli_args.go @@ -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 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...) + } +} diff --git a/cli_darwin.go b/cli_darwin.go new file mode 100644 index 0000000..634dad0 --- /dev/null +++ b/cli_darwin.go @@ -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") +} diff --git a/cli_display.go b/cli_display.go new file mode 100644 index 0000000..1af4768 --- /dev/null +++ b/cli_display.go @@ -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 +} \ No newline at end of file diff --git a/cli_linux.go b/cli_linux.go new file mode 100644 index 0000000..eae4ecc --- /dev/null +++ b/cli_linux.go @@ -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") +} diff --git a/cli_other.go b/cli_other.go new file mode 100644 index 0000000..d6b418d --- /dev/null +++ b/cli_other.go @@ -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) { } diff --git a/cli_windows.go b/cli_windows.go new file mode 100644 index 0000000..87db3f4 --- /dev/null +++ b/cli_windows.go @@ -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) { } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d7bbbd --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module louisdevie.fr/rikuri + +go 1.25.4 + +require ( + golang.org/x/sys v0.43.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ee6ec8d --- /dev/null +++ b/go.sum @@ -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= diff --git a/wrap.go b/wrap.go new file mode 100644 index 0000000..1736457 --- /dev/null +++ b/wrap.go @@ -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) +} diff --git a/wrap_test.go b/wrap_test.go new file mode 100644 index 0000000..bfc5177 --- /dev/null +++ b/wrap_test.go @@ -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) +}