diff --git a/LICENSE b/LICENSE deleted file mode 100644 index a3008d8..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Pt - -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. diff --git a/README.md b/README.md deleted file mode 100644 index 447fe29..0000000 --- a/README.md +++ /dev/null @@ -1,308 +0,0 @@ -# Tap - Terminal Argument Parsing - -[![Go Reference](https://pkg.go.dev/badge/github.com/pt-main/tap.svg)](https://pkg.go.dev/github.com/pt-main/tap) - -```bash -go get github.com/pt-main/tap -``` - -**Tap** is a lightweight library for building beautiful CLI applications in Go. -It features a simple command-based API, automatic `--flag` parsing, colored output, interactive keyboard dialogs, in-place terminal rewriting, and fully customisable help messages. - -## Features - -- **Commands** with required / optional arguments and unlimited trailing args -- **Flag parsing** - `--flag`, `--flag=value`, `--flag:value` -- **Built‑in colour support** - shortcodes like `[?GN]`, `[?RD]`, `[?YW]` - easy and readable -- **Background colours & colour stack** - advanced ANSI control for rich layouts -- **Auto‑generated help** - groups aliases, shows arguments, respects custom format -- **Fully configurable** - change the look of `help` via `ParserConfig`, add your colors to `color.Colors`, disable colors with `color.ColorEnabled = false` -- **Hide commands** from help using `DONT_SHOW` docstring -- **Subcommands** (inner parsers) -- **Verbose / debug** flags - built‑in `--verbose` and `--debug` with conditional printing -- **Interactive dialogs** - selection menus (using input/selecting) -- **In-place terminal rewriting** - update previous output without scrolling - -## Quick start - -Create a simple CLI with a `hello` command: - -```go -package main - -import ( - "fmt" - - "github.com/pt-main/tap" - "github.com/pt-main/tap/color" -) - -func helloHandler(p *tap.Parser, args []string) error { - color.PrintlnColored("[?GN]Hello[?RT], world! Args: %v", args) - return nil -} - -func main() { - cfg := tap.DefaultParserConfig() - p := tap.NewParser("demo", "Demo CLI v1.0", nil, cfg) - p.AddCommand("hello", helloHandler, "Prints a friendly greeting", nil, nil, true) - - if err := p.Main(); err != nil { - fmt.Println("Error:", err) - } -} -``` - -Run it: -demo-run - - -## Commands and arguments - -Add a command using `AddCommand`: - -```go -p.AddCommand( - name string, - handler HandlerFuncType, // func(*Parser, []string) error - docstring string, - requiredArgs []string, - optionalArgs []string, - unlimitedMaxArgs bool, -) -``` - -- **requiredArgs** - shown as `` in help. The command fails if they are missing. -- **optionalArgs** - shown as `[arg]` in help. -- **unlimitedMaxArgs** - if `true`, the command accepts any number of trailing arguments. shown as `...` in help. - -You can create aliases for existing commands using `AddAlias`: - -```go -p.AddCommand("help", helpHandler, tap.HELP_DOCS, nil, nil, false) -p.AddAlias("h", "help") // now "h" works exactly like "help" -``` -The alias inherits all properties (handler, arguments, docstring) from the original command. - -### Example - -```go -p.AddCommand("copy", - copyHandler, - "Copy source to destination", - []string{"src", "dst"}, // required - []string{"force"}, // optional - true, // unlimited -) -``` - -Help output would show: -``` -copy , , [force]... -``` - -Use `tap.DONT_SHOW` as the docstring: -```go -p.AddCommand("internal", internalHandler, tap.DONT_SHOW, nil, nil, false) -``` -This command will work but will never appear in the help output. - -Use `tap.DEFAULT_CMD` as name to create handler working when `args[0]` is not command (for usage like `cli_name [args...]`, not `cli_name command_name [args...]`). Documentation of 'default' command shows after about. - -## Sub commands - -You can create sub-commands: - -```go -p.AddSubcommand(name, tap.NewParser(...)) -``` - -In call subcomand parser calls `subcommand.Parse(args)`. - -Help show only name and about of subcommand. - -## Flags - -Flags are written as `--flag` or `--key=value` (also `--key:value`). -They are parsed automatically and stored in `p.Flags` (a `map[string]string`). A flag without a value gets an empty string. - -Built‑in flags: -- `--verbose` - enables verbose output (messages printed with `p.Print("verbose", ...)`) -- `--debug` - enables debug output (similar) - -Your handlers can read flags directly: - -```go -func myHandler(p *tap.Parser, args []string) error { - if val, ok := p.Flags["output"]; ok { - fmt.Println("Output file:", val) - } - return nil -} -``` - -Use `p.Print(flag, format, ...)` to output messages only when a specific flag (e.g., `--verbose` or `--debug`) is present: - -```go -func myHandler(p *tap.Parser, args []string) error { - p.Print("verbose", "Starting with args: %v", args) - p.Print("debug", "Debug info...") - // ... -} -``` - -The output is automatically prefixed with the flag name and coloured. - -## Colors - -color-demo - -You can just write `[?COLOR]` with uppercased color name from list to set color. Like `[?RED]` for red. - -All colors: Bold, Underline, Reset, Black, Red, Green, Yellow, Blue, Magenta, Cyan. - -Also you can set color using first and last letters of color. Like `[?RD]` for red. - -Bright variants: `[?BRED]`, `[?BRD]`, ... - -Use them with `color.PrintlnColored` or `color.PrintColored`: - -```go -color.PrintlnColored("[?GN]Success[?RT] - file saved as [?YW]%s[?RT]", filename) -``` - -To disable colours globally: - -```go -color.ColorEnabled = false -``` - -You can set color to string with `color.Set`: -```go -text := color.Set("[?RD]Test") -``` -(Reset will be auto pasted in the end of text) - -### Background colours - -Background colours use the `BACK` prefix or `BK` shortcode: - -```go -color.PrintlnColored("[?BKRD] ERROR: [?RT] error text...") -``` - -Available all colors for text except bold and underlinne. - -### Colour stack - -You can restore the previous colour with `[?<]` (or `[?BACK]`) and clear the stack with `[?SRT]` (or `[?SRESET]`): - -```go -color.PrintlnColored("[?BE][?UE]test [?BD]bold [?RT][?<]restored") -``` - - -## Interactive dialogs - -Tap includes a small utility for interactive user prompts. It supports arrow-key navigation and validated text input. - -```go -import "github.com/pt-main/tap/utils" - -// Arrow-key selection -dialogue := utils.NewDialogue(utils.ArrowsDialogueType, "[?CN]Select action:[?RT]") -choice, err := dialogue.Run([]string{"install", "update", "remove"}) -// Navigate with up/down, press enter to confirm, esc to cancel. -``` - -For simple typed input with validation against allowed variants: - -```go -inputDlg := utils.NewDialogue(utils.InputDialogueType, "Enter mode (fast/slow): ") -mode, err := inputDlg.Run([]string{"fast", "slow"}) -``` - -## Rewriting terminal output - -For in-place updates (progress bars, live menus, spinners) use `Rewriter`: - -```go -rw := utils.NewRewriter() -rw.Write("[?YW]Loading... 0%[?RT]") -// ... later ... -rw.Write("[?GN]Loading... 100%[?RT]") // replaces the previous line without scrolling -``` - -## Customising the help output - -Create a `ParserConfig` and pass it to `NewParser`. -All fields support format strings - use `%s` for the command name or argument list. - -```go -cfg := tap.NewParserConfig( - "[?CN]>>> Command [?RT]%s[?CN] <<<[?RT]", - "[?CN]Args:[?RT]", - " %s", - "[?CN]Description:[?RT]", - " %s", - "[?CN]---[?RT]", -) -p := tap.NewParser("mycli", "My tool", nil, cfg) -``` - -If you pass an empty string for any field, the default (coloured, nice looking) will be used. - -## Grouping commands / aliases - -If multiple commands share the **same docstring**, they are displayed together in help: - -```go -helpDocs := "Show help" -p.AddCommand("help", helpHandler, helpDocd, nil, nil, false) -p.AddCommand("h", helpHandler, helpDocd, nil, nil, false) // Equals to p.AddAlias("h", help) -``` - -Help shows: `[help / h]` - -## Full example - -A minimal but complete CLI with multiple commands: - -```go -package main - -import ( - "fmt" - "os" - "github.com/pt-main/tap" - "github.com/pt-main/tap/color" -) - -func main() { - cfg := tap.DefaultParserConfig() - p := tap.NewParser("myapp", "My application v0.1", nil, cfg) - - p.AddCommand("greet", func(p *tap.Parser, args []string) error { - name := "world" - if len(args) > 0 { - name = args[0] - } - color.PrintlnColored("[?GN]Hello, %s![?RT]", name) - return nil - }, "Say hello", []string{}, []string{"name"}, false) - - p.AddCommand("print", func(p *tap.Parser, args []string) error { - color.PrintlnColored("[?YW]%s[?RT]", args[0]) - return nil - }, "Print first argument", []string{"text"}, nil, false) - - if err := p.Main(); err != nil { - fmt.Fprintln(os.Stderr, "Fatal:", err) - os.Exit(1) - } -} -``` - -## License - -MIT - see [LICENSE](LICENSE) file. diff --git a/colFmt/cmd/main.go b/colFmt/cmd/main.go new file mode 100644 index 0000000..de4d7a4 --- /dev/null +++ b/colFmt/cmd/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "os" + + "github.com/pt-main/colFmt/lib" + "github.com/pt-main/lc/engine/core" +) + +func main() { + if a := os.Args; len(a) > 1 { + printErr := func(err error) { + fmt.Printf("{%s}{Err=%s}", a[1], err.Error()) + } + l, err := lib.NewLanguage() + if err != nil { + printErr(err) + return + } + err = l.ProcessString(a[1]) + if err != nil { + printErr(err) + return + } + uep, _ := l.GetUEP() + res, err := core.GetStringRes(uep.Generator, "") + if err != nil { + printErr(err) + return + } + fmt.Print(res) + } +} diff --git a/colFmt/go.mod b/colFmt/go.mod new file mode 100644 index 0000000..5bfb027 --- /dev/null +++ b/colFmt/go.mod @@ -0,0 +1,9 @@ +module github.com/pt-main/colFmt + +go 1.24.13 + +require ( + github.com/dlclark/regexp2 v1.12.0 // indirect + github.com/pt-main/lc v1.4.2 // indirect + github.com/pt-main/tap v1.4.5 // indirect +) diff --git a/colFmt/go.sum b/colFmt/go.sum new file mode 100644 index 0000000..e3c2f6a --- /dev/null +++ b/colFmt/go.sum @@ -0,0 +1,6 @@ +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/pt-main/lc v1.4.2 h1:1+UiNubv+7R/p++pDvjvX+7qa1rSvAf5JV1QH4fJwk0= +github.com/pt-main/lc v1.4.2/go.mod h1:YNbfNwM3eO/xHarctZ288mYFKFv4X9mBsDzpgogByCs= +github.com/pt-main/tap v1.4.5 h1:g6W1aIx7qHhVmugicqGRvk5UugpyGWDn/ip57iCIK6E= +github.com/pt-main/tap v1.4.5/go.mod h1:ULQUJ/+8VIji9oq26pr1cmbXv+VUlhjsvq1n/vd4f3I= diff --git a/colFmt/lib/handlers.go b/colFmt/lib/handlers.go new file mode 100644 index 0000000..af3dd7b --- /dev/null +++ b/colFmt/lib/handlers.go @@ -0,0 +1,73 @@ +package lib + +import ( + "fmt" + "strings" + + "github.com/pt-main/lc/engine" + "github.com/pt-main/lc/engine/core" + "github.com/pt-main/lc/parsing/stringParsing" + "github.com/pt-main/tap/color" +) + +func Color(se *engine.StringEngine, pn *stringParsing.ParsedNode) error { + i := pn.Metadata["value"].(string) + setter, err := core.ScopeGet[*color.Setter](se.UEP.Scope, "setter") + if err != nil { + return err + } + s := "" + for _, col := range strings.Split(i, ";") { + s += "[?" + col + "]" + } + s = setter.Set(s) + return se.UEP.Generator.AddString(s, "main") +} + +func Setup(se *engine.StringEngine, pn *stringParsing.ParsedNode) error { + i := pn.Metadata["value"].(string) + setter, err := core.ScopeGet[*color.Setter](se.UEP.Scope, "setter") + if err != nil { + return err + } + for _, param := range strings.Split(i, ";") { + switch param { + case "ColEnable", "Color+", "C+": + *setter.ColorEnabled = true + case "ColDisable", "Color-", "C-": + *setter.ColorEnabled = false + case "Col", "Color", "C": + *setter.ColorEnabled = !(*setter.ColorEnabled) + case "AddReset+", "AR+": + setter.AddReset = true + case "AddReset-", "AR-": + setter.AddReset = false + case "AddReset", "AR": + setter.AddReset = !setter.AddReset + default: + return fmt.Errorf("Invalid param: %s", param) + } + } + return nil +} + +func Manual(se *engine.StringEngine, pn *stringParsing.ParsedNode) error { + s := pn.Metadata["value"].(string) + setter, err := core.ScopeGet[*color.Setter](se.UEP.Scope, "setter") + if err != nil { + return err + } + if !(*setter.ColorEnabled) { + return nil + } + if len(s) == 0 { + s = "0" + } + s = fmt.Sprintf("\033[%sm", s) + return se.UEP.Generator.AddString(s, "main") +} + +func Raw(se *engine.StringEngine, pn *stringParsing.ParsedNode) error { + s := pn.Metadata["value"].(string) + return se.UEP.Generator.AddString(s, "main") +} diff --git a/colFmt/lib/main.go b/colFmt/lib/main.go new file mode 100644 index 0000000..1780bd2 --- /dev/null +++ b/colFmt/lib/main.go @@ -0,0 +1,56 @@ +package lib + +import ( + "github.com/dlclark/regexp2" + "github.com/pt-main/lc" + "github.com/pt-main/lc/parsing/stringParsing" + "github.com/pt-main/lc/public" + "github.com/pt-main/tap/color" +) + +func NewLexer() *stringParsing.Lexer { + return stringParsing.NewLexer( + []stringParsing.LexerRule{ + { + Type: "SETUP", + Pattern: regexp2.MustCompile(`\[[sS]/(?[^\]]*)\]`, 0), + }, + { + Type: "COLOR", + Pattern: regexp2.MustCompile(`\[[cC]/(?[^\]]*)\]`, 0), + }, + { + Type: "MANUAL", + Pattern: regexp2.MustCompile(`\[[mM]/(?[^\]]*)\]`, 0), + }, + { + Type: "RAW", + Pattern: regexp2.MustCompile(`(?(?:[^[]|\[(?![sScCmM]/))+)`, 0), + }, + }, + &stringParsing.LexerConfig{ + UseBracketBalance: false, + }, + ) +} + +func NewLanguage() (*lc.EngineUniversal, error) { + e, err := lc.NewEngineBuilder(public.StringEngineType, public.StringResType). + WithStringParser(NewLexer()). + WithDefaultEvents(true). + WithPipeline([]string{"main"}). + Build() + if err != nil { + return nil, err + } + uep, err := e.GetUEP() + if err != nil { + return nil, err + } + uep.Scope["setter"] = color.NewSetter(false, &color.ColorEnabled) + e.NewCommandString("SETUP", Setup, "") + e.NewCommandString("COLOR", Color, "") + e.NewCommandString("MANUAL", Manual, "") + e.NewCommandString("RAW", Raw, "") + return e, nil +} diff --git a/color/colors.go b/color/colors.go deleted file mode 100644 index b616c75..0000000 --- a/color/colors.go +++ /dev/null @@ -1,101 +0,0 @@ -// Package color provides ANSI color formatting for terminal output. -// It supports short color codes like [?GN] (green), [?RD] (red), [?BOLD], etc. -// Coloring can be globally disabled by setting color.ColorEnabled = false. -package color - -// Color codes for ANSI escape sequences. -const ( - black = "\033[30m" - red = "\033[31m" - green = "\033[32m" - yellow = "\033[33m" - blue = "\033[34m" - magenta = "\033[35m" - cyan = "\033[36m" - white = "\033[37m" - - brightBlack = "\033[90m" - brightRed = "\033[91m" - brightGreen = "\033[92m" - brightYellow = "\033[93m" - brightBlue = "\033[94m" - brightMagenta = "\033[95m" - brightCyan = "\033[96m" - brightWhite = "\033[97m" - - // Background colours (standard) - blackBg = "\033[40m" - redBg = "\033[41m" - greenBg = "\033[42m" - yellowBg = "\033[43m" - blueBg = "\033[44m" - magentaBg = "\033[45m" - cyanBg = "\033[46m" - whiteBg = "\033[47m" - - // Background colours (bright) - brightBlackBg = "\033[100m" - brightRedBg = "\033[101m" - brightGreenBg = "\033[102m" - brightYellowBg = "\033[103m" - brightBlueBg = "\033[104m" - brightMagentaBg = "\033[105m" - brightCyanBg = "\033[106m" - brightWhiteBg = "\033[107m" - - reset = "\033[0m" - bold = "\033[1m" - underline = "\033[4m" -) - -// Colors maps short color codes (uppercase) to ANSI escape sequences. -// Supported codes: -// - Text foreground: BOLD/BD, UNDERLINE/UE, RESET/RT, -// basic colours (BLACK/BK, RED/RD, GREEN/GN, YELLOW/YW, BLUE/BE, MAGENTA/MA, CYAN/CN), -// bright variants (BBLACK/BBK, BRED/BRD, BGREEN/BGN, BYELLOW/BYW, BBLUE/BBE, BMAGENTA/BMA, BCYAN/BCN) -// - Background colours (full name: BACK, short: BK): -// BACKBLACK/BKBK, BACKRED/BKRD, BACKGREEN/BKGN, BACKYELLOW/BKYW, BACKBLUE/BKBE, BACKMAGENTA/BKMA, BACKCYAN/BKCN, -// and bright variants: BACKBBLACK/BKBBK, BACKBRED/BKBRD, BACKBGREEN/BKBGN, BACKBYELLOW/BKBYW, BACKBBLUE/BKBBE, BACKBMAGENTA/BKBMA, BACKBCYAN/BKBCN. -var Colors = map[string]string{ - "BOLD": bold, "BD": bold, - "UNDERLINE": underline, "UE": underline, - "RESET": reset, "RT": reset, - - // Text foreground - "BLACK": black, "BK": black, - "RED": red, "RD": red, - "GREEN": green, "GN": green, - "YELLOW": yellow, "YW": yellow, - "BLUE": blue, "BE": blue, - "MAGENTA": magenta, "MA": magenta, - "CYAN": cyan, "CN": cyan, - - "BBLACK": brightBlack, "BBK": brightBlack, - "BRED": brightRed, "BRD": brightRed, - "BGREEN": brightGreen, "BGN": brightGreen, - "BYELLOW": brightYellow, "BYW": brightYellow, - "BBLUE": brightBlue, "BBE": brightBlue, - "BMAGENTA": brightMagenta, "BMA": brightMagenta, - "BCYAN": brightCyan, "BCN": brightCyan, - - // Background colours - "BACKBLACK": blackBg, "BKBK": blackBg, - "BACKRED": redBg, "BKRD": redBg, - "BACKGREEN": greenBg, "BKGN": greenBg, - "BACKYELLOW": yellowBg, "BKYW": yellowBg, - "BACKBLUE": blueBg, "BKBE": blueBg, - "BACKMAGENTA": magentaBg, "BKMA": magentaBg, - "BACKCYAN": cyanBg, "BKCN": cyanBg, - - "BACKBBLACK": brightBlackBg, "BKBBK": brightBlackBg, - "BACKBRED": brightRedBg, "BKBRD": brightRedBg, - "BACKBGREEN": brightGreenBg, "BKBGN": brightGreenBg, - "BACKBYELLOW": brightYellowBg, "BKBYW": brightYellowBg, - "BACKBBLUE": brightBlueBg, "BKBBE": brightBlueBg, - "BACKBMAGENTA": brightMagentaBg, "BKBMA": brightMagentaBg, - "BACKBCYAN": brightCyanBg, "BKBCN": brightCyanBg, - - // Other - "BACK": "", "<": "", - "SRESET": "", "SRT": "", -} diff --git a/color/print.go b/color/print.go deleted file mode 100644 index 3d2bb70..0000000 --- a/color/print.go +++ /dev/null @@ -1,17 +0,0 @@ -package color - -import "fmt" - -// PrintColored formats the string using fmt.Sprintf, replaces color codes, -// and writes the result to stdout without a trailing newline. -func PrintColored(format string, args ...interface{}) { - formatted := fmt.Sprintf(format, args...) - fmt.Print(Set(formatted)) -} - -// PrintlnColored formats the string using fmt.Sprintf, replaces color codes, -// and writes the result to stdout followed by a newline. -func PrintlnColored(format string, args ...interface{}) { - formatted := fmt.Sprintf(format, args...) - fmt.Println(Set(formatted)) -} diff --git a/color/set.go b/color/set.go deleted file mode 100644 index 551db67..0000000 --- a/color/set.go +++ /dev/null @@ -1,83 +0,0 @@ -package color - -import ( - "slices" - "strings" -) - -// ColorEnabled globally enables or disables ANSI color output. -// When false, all color codes are stripped from the output. -var ColorEnabled = true - -// Replace color codes -func ReplaceColors(text string) string { - result := text - for code, ansi := range Colors { - if !ColorEnabled { - ansi = "" - } - result = strings.ReplaceAll(result, FormColorPlaceholder(code), ansi) - } - return result -} - -// Place color code (like 'RD', 'RED', etc.) to form for replace to color -// Like 'rd' -> '[?RD]' -func FormColorPlaceholder(code string) string { - return "[?" + strings.ToUpper(code) + "]" -} - -// Set replaces short color codes like [?RED] or [?GN] in the input string -// with the corresponding ANSI escape sequences. If ColorEnabled is false, -// the codes are removed entirely. The ANSI reset code is automatically -// appended at the end when colors are enabled. -// -// Use 'BACK' (or '<') code to append last colors. -// -// Use 'SRESET' (or 'SRT') code to reset color stack. -// -// Example: -// -// "[?BE][?UE]test [?BD]bold [?RT][?<]string" equal to "[?BE]test [?BD]bold [?RT][?BE][?UE]string" -func Set(text string) string { - BackVariants := []string{"BACK", "<"} - SrtVariants := []string{"SRESET", "SRT"} - result := text - if !ColorEnabled { - return ReplaceColors(result) - } - colorStack := []string{} - for code, ansi := range Colors { - if slices.Contains(BackVariants, code) { - replace := "" - for _, code := range colorStack { - replace += Colors[code] - } - result = strings.Replace(result, code, replace, 1) - } else if slices.Contains(SrtVariants, code) { - result = strings.Replace(result, code, "", 1) - colorStack = []string{} - } else { - result = strings.ReplaceAll(result, FormColorPlaceholder(code), ansi) - if ansi == Colors["RT"] { - if len(colorStack)-1 >= 0 { - colorStack = colorStack[:len(colorStack)-1] - } - } else { - colorStack = append(colorStack, code) - } - } - - } - return result + Colors["RT"] -} - -// ConvertColored applies color formatting to each string in the slice -// using Set and returns a new slice with the colored strings. -func ConvertColored(text ...string) []string { - result := []string{} - for arg := range text { - result = append(result, Set(text[arg])) - } - return result -} diff --git a/config.go b/config.go deleted file mode 100644 index aaf73c1..0000000 --- a/config.go +++ /dev/null @@ -1,90 +0,0 @@ -// Package tap provides a lightweight command-line argument parser with support -// for commands, flags, colored output, and customizable help messages. -package tap - -// DONT_SHOW is a special docstring value that hides the command from the help output. -// The command remains functional but will not appear in the auto-generated help. -const DONT_SHOW = "#[DON'T SHOW]#" - -// DEFAULT_CMD is a reserved command name. -// It is used when no command is specified as the first argument, -// allowing your CLI to be invoked in two ways: cli args... (implicit default) -// or cli cmd args... (explicit command). -// Note: documentation of default command is displayed after about message. -const DEFAULT_CMD = "#[DEFAULT CMD]#" - -// HELP_DOCS is the docstring used by the built‑in help command. -// Commands sharing this docstring will be grouped together as aliases. -const HELP_DOCS = "Generate and print help message" - -// ParserConfig defines the formatting templates for the auto-generated help message. -// Each field is a format string that may contain "%s" placeholders for dynamic content. -// If an empty string is passed to NewParserConfig, the corresponding default format will be used. -type ParserConfig struct { - help_command_block_fmt string - help_subcommand_block_fmt string - help_args_header_block_fmt string - help_args_data_block_fmt string - help_docs_header_block_fmt string - help_docs_data_block_fmt string - help_end_block_fmt string -} - -// NewParserConfig creates a new ParserConfig with the given format strings. -// Any empty string parameter will be replaced with a sensible default. -// Parameters: -// - help_command_block_fmt: format for the command name block (e.g., "╭─────── Command [%s]"). -// - help_subcommand_block_fmt: format for the subcommand name block (e.g., "╭─────── Subcommand [%s]"). -// - help_args_header_block_fmt: format for the arguments section header. -// - help_args_data_block_fmt: format for each argument line. -// - help_docs_header_block_fmt: format for the description section header. -// - help_docs_data_block_fmt: format for each line of the description. -// - help_end_block_fmt: format for the closing block. -// -// Returns a populated ParserConfig. -func NewParserConfig( - help_command_block_fmt string, - help_subcommand_block_fmt string, - help_args_header_block_fmt string, - help_args_data_block_fmt string, - help_docs_header_block_fmt string, - help_docs_data_block_fmt string, - help_end_block_fmt string, -) ParserConfig { - if help_command_block_fmt == "" { - help_command_block_fmt = "[?GN]╭─────── Command[?RT] [%s]" - } - if help_subcommand_block_fmt == "" { - help_subcommand_block_fmt = "[?GN]╭─────── Subommand[?RT] [%s]" - } - if help_args_header_block_fmt == "" { - help_args_header_block_fmt = "[?GN]⎬─ Args:[?RT]" - } - if help_args_data_block_fmt == "" { - help_args_data_block_fmt = "[?GN]│[?RT] %s" - } - if help_docs_header_block_fmt == "" { - help_docs_header_block_fmt = "[?GN]⎬─ Desc:[?RT]" - } - if help_docs_data_block_fmt == "" { - help_docs_data_block_fmt = "[?GN]│[?RT] %s" - } - if help_end_block_fmt == "" { - help_end_block_fmt = "[?GN]╰───────[?RT]" - } - - return ParserConfig{ - help_command_block_fmt: help_command_block_fmt, - help_subcommand_block_fmt: help_subcommand_block_fmt, - help_args_header_block_fmt: help_args_header_block_fmt, - help_args_data_block_fmt: help_args_data_block_fmt, - help_docs_header_block_fmt: help_docs_header_block_fmt, - help_docs_data_block_fmt: help_docs_data_block_fmt, - help_end_block_fmt: help_end_block_fmt, - } -} - -// Create default parser config. -func DefaultParserConfig() ParserConfig { - return NewParserConfig("", "", "", "", "", "", "") -} diff --git a/core/scope.go b/core/scope.go deleted file mode 100644 index 3ef9526..0000000 --- a/core/scope.go +++ /dev/null @@ -1,18 +0,0 @@ -package core - -import "errors" - -type ScopeType map[string]interface{} - -func ScopeGet[T any](st ScopeType, what string) (T, error) { - var nul T - val, ok := st[what] - if !ok { - return nul, errors.New("Scope.Get: Invalid key: " + what) - } - res, ok := val.(T) - if !ok { - return nul, errors.New("Scope.Get: Invalid type for key: " + what) - } - return res, nil -} diff --git a/core/utils.go b/core/utils.go deleted file mode 100644 index d028dbb..0000000 --- a/core/utils.go +++ /dev/null @@ -1,52 +0,0 @@ -package core - -import "strings" - -// utils is an internal helper type providing argument parsing functionality. -type Utils struct{} - -func (u *Utils) ParseFlag(el string) (string, string) { - el = el[2:] - - var key, value string - var hasValue bool - - if strings.Contains(el, "=") { - parts := strings.SplitN(el, "=", 2) - key, value = parts[0], parts[1] - hasValue = true - } else if strings.Contains(el, ":") { - parts := strings.SplitN(el, ":", 2) - key, value = parts[0], parts[1] - hasValue = true - } else { - key = el - hasValue = false - } - - if hasValue { - return key, value - } else { - return key, "" - } -} - -// parse_args processes a slice of command-line arguments. -// It detects flags prefixed with "--" (e.g., "--flag", "--key=value", "--key:value"). -// Flags without a value are stored with an empty string. -// Returns a map of flag names to their values, and a slice of non-flag arguments. -func (u *Utils) ParseArgs(argv []string) (map[string]string, []string) { - flags := make(map[string]string) - var result []string - - for _, el := range argv { - if strings.HasPrefix(el, "--") { - key, val := u.ParseFlag(el) - flags[key] = val - } else { - result = append(result, el) - } - } - - return flags, result -} diff --git a/go.mod b/go.mod deleted file mode 100644 index c1e5910..0000000 --- a/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/pt-main/tap - -go 1.24.13 - -require ( - github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 24996dc..0000000 --- a/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= -github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/help.go b/help.go deleted file mode 100644 index c2f2447..0000000 --- a/help.go +++ /dev/null @@ -1,91 +0,0 @@ -package tap - -import ( - "slices" - "strings" - - "github.com/pt-main/tap/color" -) - -func help_form_args(el command) string { - if el.optional_args != nil || el.required_args != nil || el.unlimited_max_args { - args_doc := "" - if el.required_args != nil && len(el.required_args) != 0 { - for arg := range el.required_args { - args_doc += "<[?RD]" + el.required_args[arg] + "[?RT]>" - if arg != (len(el.required_args) - 1) { - args_doc += ", " - } - } - } - if el.optional_args != nil && len(el.optional_args) != 0 { - if len(args_doc) > 2 { - args_doc += ", " - } - for arg := range el.optional_args { - args_doc += "[[?BE]" + el.optional_args[arg] + "[?RT]]" - if arg != (len(el.optional_args) - 1) { - args_doc += ", " - } - } - } - if el.unlimited_max_args { - args_doc += "..." - } - return args_doc - } - return "" -} - -// help_cmd_handler implements the built‑in help command. -// It prints a formatted help message listing all visible commands, -// their arguments (required/optional), and descriptions. -func help_cmd_handler(p *Parser, _ []string) error { - p._print_about() - docstrings := []string{} - for key := range p._commands { - el := p._commands[key] - docs := strings.Split(el.docstring, "\n") - if el.docstring == DONT_SHOW || el.name == DEFAULT_CMD { - docstrings = append(docstrings, el.docstring) - } - if slices.Index(docstrings, el.docstring) == -1 { - cmds := []string{} - for key := range p._commands { - if p._commands[key].docstring == el.docstring { - cmds = append(cmds, p._commands[key].name) - } - } - commands := "[?YW]" - for idx, cmd := range cmds { - commands += cmd - if idx != (len(cmds) - 1) { - commands += " [?RT]/[?YW] " - } - } - commands += "[?RT]" - color.PrintlnColored(p._config.help_command_block_fmt, commands) - args_doc := help_form_args(el) - if args_doc != "" { - color.PrintlnColored(p._config.help_args_header_block_fmt) - color.PrintlnColored(p._config.help_args_data_block_fmt, args_doc) - } - color.PrintlnColored(p._config.help_docs_header_block_fmt) - for line := range docs { - color.PrintlnColored(p._config.help_docs_data_block_fmt, docs[line]) - } - color.PrintlnColored(p._config.help_end_block_fmt) - docstrings = append(docstrings, el.docstring) - } - } - for key := range p._sub_commands { - el := p._sub_commands[key] - color.PrintlnColored(p._config.help_subcommand_block_fmt, el._cli_name) - color.PrintlnColored(p._config.help_docs_header_block_fmt) - for _, line := range strings.Split(el._about_info, "\n") { - color.PrintlnColored(p._config.help_docs_data_block_fmt, line) - } - color.PrintlnColored(p._config.help_end_block_fmt) - } - return nil -} diff --git a/main.go b/main.go deleted file mode 100644 index 29be18b..0000000 --- a/main.go +++ /dev/null @@ -1,3 +0,0 @@ -package tap - -const Version = "1.4.5" diff --git a/parser.go b/parser.go deleted file mode 100644 index 37ab22b..0000000 --- a/parser.go +++ /dev/null @@ -1,210 +0,0 @@ -package tap - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/pt-main/tap/color" - "github.com/pt-main/tap/core" -) - -// HandlerFuncType defines the signature for command handler functions. -// It receives the parser instance and the slice of command arguments. -// Returns an error if the command execution fails. -type HandlerFuncType func(*Parser, []string) error - -// Command stores internal metadata for a registered command. -type command struct { - name string - handler HandlerFuncType - docstring string - required_args []string - optional_args []string - unlimited_max_args bool -} - -/* -# Tap - Terminal Argument Parsing - -This parser is the main object in tap. - -Main methods: - -- AddCommand(name string, handler HandlerFuncType) - -- Main() - start parser -*/ -type Parser struct { - _cli_name string - _about_info string - _parser_flags map[string]bool - Flags map[string]string - Scope core.ScopeType - _commands map[string]command - _sub_commands map[string]*Parser - _config ParserConfig -} - -// NewParser creates a new Parser instance. -// Parameters: -// - cli_name: name of the CLI application (used in help). -// - about: informational text printed when no command is given. -// - help_commands: slice of command names that trigger the help handler. -// If nil, defaults to []string{"help", "h"}. -// - config: ParserConfig controlling help message formatting. -// -// Returns a pointer to the initialized Parser. -func NewParser(cli_name string, about string, help_commands []string, config ParserConfig) *Parser { - p := Parser{ - _cli_name: cli_name, - _about_info: about, - _parser_flags: map[string]bool{ - "debug": false, "verbose": false, - }, - _commands: map[string]command{}, - _sub_commands: make(map[string]*Parser), - Flags: map[string]string{}, - _config: config, - } - if help_commands == nil { - help_commands = []string{"help", "h"} - } - for _, cmd := range help_commands { - p.AddCommand(cmd, help_cmd_handler, HELP_DOCS, nil, nil, false) - } - return &p -} - -// AddCommand registers a new command with the parser. -// Parameters: -// - name: command name (string used in CLI). -// - handler: function called when the command is invoked. -// - docs: description shown in help; use DONT_SHOW to hide the command. -// - required_args: slice of required argument names. -// - optional_args: slice of optional argument names. -// - unlimited_max_args: if true, command accepts any number of trailing arguments. -func (p *Parser) AddCommand( - name string, - handler HandlerFuncType, - docs string, - required_args []string, - optional_args []string, - unlimited_max_args bool, -) { - p.__print_verbose( - "Adding command '%s' with %v required and %v optional args", - name, required_args, optional_args, - ) - p._commands[name] = command{ - name: name, - handler: handler, - docstring: docs, - required_args: required_args, - optional_args: optional_args, - unlimited_max_args: unlimited_max_args, - } -} - -// AddSubCommand registers a nested sub-parser for a given subcommand name. -// -// This method stores the provided Parser instance under the specified name, -// automatically integrate it into the main command dispatch flow. -// -// Parameters: -// - name: the subcommand name. -// - parser: the Parser instance that will handle the subcommand. -func (p *Parser) AddSubcommand(name string, parser *Parser) { - p._sub_commands[name] = parser -} - -// SubCommand retrieves a previously registered sub-parser by name. -// -// It returns the Parser pointer and a nil error if the name exists; -// otherwise, it returns an error describing the invalid name. -func (p *Parser) Subcommand(name string) (err error, pr *Parser) { - var ok bool - pr, ok = p._sub_commands[name] - if !ok { - err = fmt.Errorf("Invalid subcommand name: %v", name) - return - } - return -} - -// Add alias for command. -func (p *Parser) AddAlias(aliasName, cmdName string) error { - cmdMap, ok := p._commands[cmdName] - if !ok { - return errors.New("[?RD]Can't add alias[?RT]: \nCommand not found") - } - p._commands[aliasName] = cmdMap - return nil -} - -// Print outputs a formatted message only if the given flag (e.g., "debug", "verbose") is enabled. -// The message can contain color shortcodes. Each newline is prefixed with the flag’s name for alignment. -func (p *Parser) Print(flag string, format string, args ...any) { - spaces := strings.Repeat(" ", len(flag)) - format = strings.ReplaceAll(format, "\n", "\n"+spaces+" [?GN]=>[?RT] ") - if p._parser_flags[flag] { - color.PrintlnColored("[?RD]"+strings.ToUpper(flag)+"[?RT] [?GN]=>[?RT] "+format, args...) - } -} - -// Parse args. -func (p *Parser) Parse(cmdArgs []string) error { - argv := p._parse_args(cmdArgs) - p.__check_flags() - if len(argv) < 1 { - p._print_about() - var help_name string - for _, el := range p._commands { - if el.docstring == HELP_DOCS { - help_name = el.name - break - } - } - color.PrintlnColored( - "[?RD]Has no command.[?RT] Type [[?YW]%s[?RT]] for help.", - help_name, - ) - return errors.New("[?RD]No command provided[?RT]") - } - p.__print_verbose("Finding and calling command...") - cmd := argv[0] - args := argv[1:] - p.__print_verbose( - "Call '%s' with %v args...", - cmd, args, - ) - err := p._call_command(cmd, args) - p.__print_verbose("Return after call: %v", err) - if err != nil { - err = p._call_basic(argv) - if err != nil { - p.Print("debug", "'%s' cmd handler call with '%s' args end with error: %v", cmd, args, err) - return fmt.Errorf("[?RD]Command [?BBK]%q[?RD] failed: \n%w", cmd, err) - } - } - return nil -} - -// Main is the primary entry point of the parser. -// It parses os.Args[1:], extracts flags, finds the command, and executes the corresponding handler. -// Returns an error if no command is provided, the command is unknown, or the handler fails. -func (p *Parser) Main() error { - err := p.Parse(os.Args[1:]) - if err != nil { - errstart := "[?RT][?YW]->[?RT] [?BBK]|[?RT] " - return fmt.Errorf(color.Set( - fmt.Sprintf("[?YW]Execution stopped[?RT]: \n"+errstart+"%v", - strings.ReplaceAll( - err.Error(), - "\n", "\n"+errstart, - ), - ))) - } - return fmt.Errorf("") -} diff --git a/systems.go b/systems.go deleted file mode 100644 index 68ae3dc..0000000 --- a/systems.go +++ /dev/null @@ -1,93 +0,0 @@ -package tap - -import ( - "fmt" - - "github.com/pt-main/tap/color" - "github.com/pt-main/tap/core" -) - -func isArgsInalid(args []string, cmd command) bool { - full_length := len(cmd.optional_args) + len(cmd.required_args) - cond1 := (len(args) > full_length) && (!cmd.unlimited_max_args) - cond2 := len(args) < len(cmd.required_args) - return cond1 || cond2 -} - -// _call_command looks up the command by name and executes its handler. -// It validates argument count against required/optional definitions. -// Returns an error if the command is unknown or argument count is invalid. -func (p *Parser) _call_command(name string, args []string) error { - cmd, ok := p._commands[name] - if !ok { - if err := p._call_subcommand(name, args); err != nil { - return err - } - } - if isArgsInalid(args, cmd) { - return fmt.Errorf("Invalid argument length: %d.", len(args)) - } - return cmd.handler(p, args) -} - -func (p *Parser) _call_basic(args []string) error { - cmd, ok := p._commands[DEFAULT_CMD] - if !ok { - return fmt.Errorf("Bad input.") - } - if isArgsInalid(args, cmd) { - return fmt.Errorf("Invalid argument length: %d.", len(args)) - } - return cmd.handler(p, args) -} - -func (p *Parser) _call_subcommand(name string, args []string) error { - cmd, ok := p._sub_commands[name] - if !ok { - return fmt.Errorf("Unknown command: %s", name) - } - return cmd.Parse(args) -} - -// _parse_args extracts flags (--flag, --key=value, --key:value) from the raw argument slice. -// Flags are stored in p.Flags (value is empty string if no value was given). -// Returns the remaining non‑flag arguments. -func (p *Parser) _parse_args(argv []string) []string { - p.__print_verbose("Parsing args.") - flags, res := (&core.Utils{}).ParseArgs(argv) - p.Flags = flags - return res -} - -// __print_verbose prints a formatted message when the "verbose" flag is enabled. -func (p *Parser) __print_verbose(format string, args ...any) { - p.Print("verbose", format, args...) -} - -// __print_debug prints a formatted message when the "debug" flag is enabled. -func (p *Parser) __print_debug(format string, args ...any) { - p.Print("debug", format, args...) -} - -// _print_about prints the CLI information (name/version) stored in _about_info. -func (p *Parser) _print_about() { - p.__print_verbose("Print about") - color.PrintlnColored(p._about_info) - println(color.Set(p._commands[DEFAULT_CMD].docstring)) -} - -// __check_flags enables internal verbose/debug flags based on presence in p.Flags. -func (p *Parser) __check_flags() { - _, verbose_ok := p.Flags["verbose"] - if verbose_ok { - p._parser_flags["verbose"] = true - } - _, debug_ok := p.Flags["debug"] - if debug_ok { - p._parser_flags["debug"] = true - } - p.__print_verbose( - "Check flags by verbose and debug. \n Flags: %v, \n Parser flags: %v", - p.Flags, p._parser_flags, - ) -} diff --git a/tap/Cargo.lock b/tap/Cargo.lock new file mode 100644 index 0000000..7e87fb1 --- /dev/null +++ b/tap/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "tap-rs" +version = "0.3.0" diff --git a/tap/Cargo.toml b/tap/Cargo.toml new file mode 100644 index 0000000..ecd59e4 --- /dev/null +++ b/tap/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tap-rs" +version = "0.3.0" +edition = "2024" + +[dependencies] + +[lib] +name = "tap" +path = "src/tap/mod.rs" diff --git a/tap/resources/cmd-darwin-amd64-final b/tap/resources/cmd-darwin-amd64-final new file mode 100755 index 0000000..1814734 Binary files /dev/null and b/tap/resources/cmd-darwin-amd64-final differ diff --git a/tap/resources/cmd-darwin-arm64-final b/tap/resources/cmd-darwin-arm64-final new file mode 100755 index 0000000..adb29cd Binary files /dev/null and b/tap/resources/cmd-darwin-arm64-final differ diff --git a/tap/resources/cmd-linux-amd64-final b/tap/resources/cmd-linux-amd64-final new file mode 100755 index 0000000..57444f6 Binary files /dev/null and b/tap/resources/cmd-linux-amd64-final differ diff --git a/tap/resources/cmd-linux-arm64-final b/tap/resources/cmd-linux-arm64-final new file mode 100755 index 0000000..974d2a8 Binary files /dev/null and b/tap/resources/cmd-linux-arm64-final differ diff --git a/tap/resources/cmd-windows-amd64-final.exe b/tap/resources/cmd-windows-amd64-final.exe new file mode 100755 index 0000000..4c42e51 Binary files /dev/null and b/tap/resources/cmd-windows-amd64-final.exe differ diff --git a/tap/resources/cmd-windows-arm64-final.exe b/tap/resources/cmd-windows-arm64-final.exe new file mode 100755 index 0000000..36fadfa Binary files /dev/null and b/tap/resources/cmd-windows-arm64-final.exe differ diff --git a/tap/src/lib.rs b/tap/src/lib.rs new file mode 100644 index 0000000..5444222 --- /dev/null +++ b/tap/src/lib.rs @@ -0,0 +1 @@ +pub mod tap; \ No newline at end of file diff --git a/tap/src/main.rs b/tap/src/main.rs new file mode 100644 index 0000000..49cdf87 --- /dev/null +++ b/tap/src/main.rs @@ -0,0 +1,12 @@ +use tap::{argsparsing::ArgsP, color, engine::structs::{CmdInfo, Engine}}; + +fn main() { + let mut ap = ArgsP::new(); + let mut e = Engine::new(&mut ap); + e.new_command("cmd", CmdInfo::new(|_, a: &[&str]|{ + println!("args: {:?}", a); + None + }, vec![], vec![], true)); + println!("{:?}, {:?}", e.work(&vec!["--test", "cmd", "args", "--val:test", "-val2=val"]), e.args_p.values); + println!("{}", color::set::set("[s/C+][c/GN]test[c/RT]")) +} diff --git a/tap/src/tap/argsparsing.rs b/tap/src/tap/argsparsing.rs new file mode 100644 index 0000000..4e97bcc --- /dev/null +++ b/tap/src/tap/argsparsing.rs @@ -0,0 +1,57 @@ +use std::collections::HashMap; + +pub struct ArgsP { + pub args: Vec, + pub flags: Vec, + pub values: HashMap, +} + +impl ArgsP { + pub fn new() -> Self { + Self { + args: vec![], + flags: vec![], + values: HashMap::new(), + } + } + + fn count_leading_same_chars(s: &str) -> usize { + let mut chars = s.chars(); + match chars.next() { + Some(first) => 1 + chars.take_while(|&c| c == first).count(), + None => 0, + } + } + + fn parse_args(args: &[&str]) -> (Vec, HashMap, Vec) { + let mut flags: Vec = vec![]; + let mut values: HashMap = HashMap::new(); + let mut r_args: Vec = vec![]; + for arg in args { + let mut split: Vec<&str> = arg.splitn(2, "=").collect(); + if split.len() == 1 { + split = arg.splitn(2, ":").collect(); + } + let s0 = split[0]; + let sch = Self::count_leading_same_chars(s0); + + if s0.starts_with("-") && s0.len() > sch && sch < 3 { + if split.len() > 1 { + values.insert(s0[sch..].to_string(), split[1].to_string()); + } else { + flags.push(s0[sch..].to_string()); + } + } else { + r_args.push(arg.to_string()); + } + } + (flags, values, r_args) + } + + pub fn parse_ags_to_self(&mut self, args: &[&str]) { + let (flags, values, r_args) = Self::parse_args(args); + self.flags = flags; + self.values = values; + self.args = r_args; + } +} diff --git a/tap/src/tap/color/mod.rs b/tap/src/tap/color/mod.rs new file mode 100644 index 0000000..1ba27d5 --- /dev/null +++ b/tap/src/tap/color/mod.rs @@ -0,0 +1,2 @@ +pub mod set; +pub mod print; \ No newline at end of file diff --git a/tap/src/tap/color/print.rs b/tap/src/tap/color/print.rs new file mode 100644 index 0000000..41fbd51 --- /dev/null +++ b/tap/src/tap/color/print.rs @@ -0,0 +1,9 @@ +use crate::color::set::set; + +pub fn print(text: &str) { + print!("{}", set(text)); +} + +pub fn println(text: &str) { + print(&(text.to_owned() + "\n")) +} \ No newline at end of file diff --git a/tap/src/tap/color/set.rs b/tap/src/tap/color/set.rs new file mode 100644 index 0000000..dde4b5b --- /dev/null +++ b/tap/src/tap/color/set.rs @@ -0,0 +1,38 @@ +use std::process::Command; +use std::path::PathBuf; +use std::env::consts::{OS, ARCH}; + +fn get_binary_path() -> PathBuf { + let os_name = match OS { + "windows" => "win", + "macos" => "darwin", + other => other, + }; + + let arch_name = match ARCH { + "x86_64" => "amd64", + "aarch64" => "arm64", + other => other, + }; + + let mut binary_name = format!("cmd-{}-{}-final", os_name, arch_name); + if OS == "windows" { + binary_name.push_str(".exe"); + } + PathBuf::from("./resources").join(binary_name) +} + +pub fn set(text: &str) -> String { + let binary_path = get_binary_path(); + if !binary_path.exists() { + panic!("Binary not found: {:?}", binary_path); + } + let output = Command::new(&binary_path) + .arg(format!("\"{}\"", text.replace("\"", "\\\""))) + .output() + .expect("failed to execute process"); + + let res = String::from_utf8(output.stdout) + .expect("output is not valid UTF-8"); + res[1..(res.len()-1)].to_string() +} \ No newline at end of file diff --git a/tap/src/tap/engine/cmdinfo.rs b/tap/src/tap/engine/cmdinfo.rs new file mode 100644 index 0000000..2b46a96 --- /dev/null +++ b/tap/src/tap/engine/cmdinfo.rs @@ -0,0 +1,17 @@ +use crate::engine::structs::{CmdInfo}; + +impl<'a, H> CmdInfo<'a, H> { + pub fn new( + handler: H, + required: Vec<&'a str>, + optional: Vec<&'a str>, + unlimited: bool, + ) -> Self { + Self { + handler, + required, + optional, + unlimited, + } + } +} diff --git a/tap/src/tap/engine/dispatching.rs b/tap/src/tap/engine/dispatching.rs new file mode 100644 index 0000000..21160d4 --- /dev/null +++ b/tap/src/tap/engine/dispatching.rs @@ -0,0 +1,45 @@ +use crate::engine::structs::{Engine, CmdInfo}; + + +fn validate_args(info: &CmdInfo<'_, H>, args: &[&str]) -> Option { + let ln = args.len(); + let r = &info.required; + let o = &info.optional; + let u = info.unlimited; + if (ln > r.len() + o.len() && !u) || ln < r.len() { + return Some(format!( + "Invalid argument length: {} (must be: [{}])", + ln, r.join(", ") + )); + } + None +} + + +impl Engine<'_> { + pub fn process(&mut self, cmd: &str, args: &[&str]) -> Option { + let err: Option; + if let Some(info) = self.cmds.get(cmd) { + if let Some(err) = validate_args(info, args) { + return Some(err); + } + err = (info.handler)(self, args); + } else if let Some(info) = self.subcmds.get(cmd) { + if let Some(err) = validate_args(info, args) { + return Some(err); + } + err = (info.handler)(args); + } else if let Some(info) = &self.basic_handler { + if let Some(err) = validate_args(info, args) { + return Some(err); + } + err = (info.handler)(self, args); + } else { + err = Some("Invalid command: not found".to_string()); + } + match err { + Some(s) => return Some(format!("Process Error: {}", s)), + None => return None, + } + } +} \ No newline at end of file diff --git a/tap/src/tap/engine/engine.rs b/tap/src/tap/engine/engine.rs new file mode 100644 index 0000000..5bbbd3a --- /dev/null +++ b/tap/src/tap/engine/engine.rs @@ -0,0 +1,37 @@ +use crate::argsparsing::ArgsP; +use crate::engine::structs::{Engine, CmdInfoSubcmd, CmdInfoCmd}; +use std::collections::HashMap; + + +impl<'a> Engine<'a> { + pub fn new(args_p: &'a mut ArgsP) -> Self { + Self { + args_p, + cmds: HashMap::new(), + subcmds: HashMap::new(), + basic_handler: None, + } + } + + pub fn new_command(&mut self, name: &'a str, cmdinfo: CmdInfoCmd<'a>) { + self.cmds.insert(name, cmdinfo); + } + + pub fn new_subcommand(&mut self, name: &'a str, cmdinfo: CmdInfoSubcmd<'a>) { + self.subcmds.insert(name, cmdinfo); + } + + pub fn work(&mut self, args: &[&str]) -> Result<(), String> { + self.args_p.parse_ags_to_self(args); + if self.args_p.args.is_empty() { + return Err("No command provided".to_string()); + } + let all_args = self.args_p.args.clone(); + let cmd = all_args[0].as_str(); + let args_refs: Vec<&str> = all_args[1..].iter().map(|s| s.as_str()).collect(); + match self.process(cmd, &args_refs) { + Some(val) => Err(val), + None => Ok(()), + } + } +} \ No newline at end of file diff --git a/tap/src/tap/engine/mod.rs b/tap/src/tap/engine/mod.rs new file mode 100644 index 0000000..a4d9e29 --- /dev/null +++ b/tap/src/tap/engine/mod.rs @@ -0,0 +1,4 @@ +pub mod cmdinfo; +pub mod engine; +pub mod structs; +pub mod dispatching; \ No newline at end of file diff --git a/tap/src/tap/engine/structs.rs b/tap/src/tap/engine/structs.rs new file mode 100644 index 0000000..46c3cf7 --- /dev/null +++ b/tap/src/tap/engine/structs.rs @@ -0,0 +1,22 @@ +use crate::argsparsing::ArgsP; +use std::collections::HashMap; + +pub type HandlerType = fn(&mut Engine<'_>, &[&str]) -> Option; +pub type SubHandlerType = fn(&[&str]) -> Option; + +pub type CmdInfoCmd<'a> = CmdInfo<'a, HandlerType>; +pub type CmdInfoSubcmd<'a> = CmdInfo<'a, SubHandlerType>; + +pub struct CmdInfo<'a, H> { + pub handler: H, + pub required: Vec<&'a str>, + pub optional: Vec<&'a str>, + pub unlimited: bool, +} + +pub struct Engine<'a> { + pub args_p: &'a mut ArgsP, + pub cmds: HashMap<&'a str, CmdInfoCmd<'a>>, + pub subcmds: HashMap<&'a str, CmdInfoSubcmd<'a>>, + pub basic_handler: Option>, +} \ No newline at end of file diff --git a/tap/src/tap/mod.rs b/tap/src/tap/mod.rs new file mode 100644 index 0000000..cf895d7 --- /dev/null +++ b/tap/src/tap/mod.rs @@ -0,0 +1,3 @@ +pub mod engine; +pub mod argsparsing; +pub mod color; \ No newline at end of file diff --git a/utils/dialogue.go b/utils/dialogue.go deleted file mode 100644 index 643e4c7..0000000 --- a/utils/dialogue.go +++ /dev/null @@ -1,112 +0,0 @@ -package utils - -import ( - "fmt" - "slices" - - "github.com/eiannone/keyboard" - "github.com/pt-main/tap/color" -) - -// DialogueType represents the mode of user interaction. -type DialogueType int - -const ( - // ArrowsDialogueType enables keyboard arrow navigation. - ArrowsDialogueType DialogueType = iota - // InputDialogueType reads a typed string from stdin. - InputDialogueType -) - -// DialogueFormat holds the prompt text and interaction mode for a dialogue. -type DialogueFormat struct { - Text string - Type DialogueType -} - -// Run executes the dialogue and returns the selected variant. -func (df *DialogueFormat) Run(variants []string) (string, error) { - print(color.Set(df.Text)) - if df.Type == InputDialogueType { - return df.GetVariantByInput(variants) - } else if df.Type == ArrowsDialogueType { - return df.GetVariantByArrows(variants) - } else { - return "", fmt.Errorf("Invalid dialogue type.") - } -} - -// GetVariantByInput reads a line from stdin and validates it against allowed variants. -func (df *DialogueFormat) GetVariantByInput(variants []string) (string, error) { - var variant string - fmt.Scan(&variant) - if !slices.Contains(variants, variant) { - return "", fmt.Errorf("Invalid input: unknown variant") - } - return variant, nil -} - -// GetVariantByArrows displays an arrow-navigable menu and returns the chosen item. -func (df *DialogueFormat) GetVariantByArrows(variants []string) (string, error) { - if err := keyboard.Open(); err != nil { - return "", err - } - defer keyboard.Close() - - var variant int - variantsLength := len(variants) - - form := func() string { - result := "" - for idx, val := range variants { - if idx == variant { - result += "[?RD]* " + val + "[?RT]\n" - } else { - result += "- " + val + "\n" - } - } - return color.Set(result) - } - - rw := NewRewriter() - rw.Write(form()) - - for { - _, key, err := keyboard.GetKey() - if err != nil { - return "", err - } - - switch key { - case keyboard.KeyArrowUp: - if variant != 0 { - variant -= 1 - } - rw.Write(form()) - case keyboard.KeyArrowDown: - if variant != (variantsLength - 1) { - variant += 1 - } - rw.Write(form()) - case keyboard.KeyEsc: - fmt.Println("Exiting...") - return "", err - case keyboard.KeyEnter: - return variants[variant], nil - } - } -} - -// RunCheckVariant runs the dialogue and reports whether the result matches the expected variant. -func (df *DialogueFormat) RunCheckVariant(variants []string, variant string) (bool, error) { - v, err := df.Run(variants) - return v == variant, err -} - -// NewDialogue creates a DialogueFormat with the given type and prompt text. -func NewDialogue(dtype DialogueType, form string) *DialogueFormat { - return &DialogueFormat{ - Type: dtype, - Text: form, - } -} diff --git a/utils/rewriter.go b/utils/rewriter.go deleted file mode 100644 index 1c97fb7..0000000 --- a/utils/rewriter.go +++ /dev/null @@ -1,46 +0,0 @@ -package utils - -import ( - "fmt" - "strings" -) - -// Rewriter provides in-place terminal output rewriting using ANSI escape codes. -type Rewriter struct { - lastLines int -} - -func NewRewriter() *Rewriter { - return &Rewriter{} -} - -// Write replaces the previously written text with new content without scrolling. -func (r *Rewriter) Write(text string) { - text = strings.TrimSpace(text) - lines := strings.Split(text, "\n") - if len(lines) > 0 && lines[len(lines)-1] == "" { - lines = lines[:len(lines)-1] - } - currentLines := len(lines) - 1 - if r.lastLines > 0 { - fmt.Printf("\033[%dA", r.lastLines) - } - for i, line := range lines { - fmt.Print("\033[2K\r") - if i < len(lines)-1 { - fmt.Println(line) - } else { - fmt.Print(line) - } - } - if currentLines < r.lastLines { - extra := r.lastLines - currentLines - for i := 0; i < extra; i++ { - fmt.Print("\033[1B") - fmt.Print("\033[2K\r") - } - - fmt.Printf("\033[%dA", extra) - } - r.lastLines = currentLines -}