From 46d032cd21b0e8e2c94a32333d3805ec76980cca Mon Sep 17 00:00:00 2001 From: ewy Date: Mon, 1 Jun 2026 18:47:44 +0200 Subject: add man(1) generation --- .gitignore | 3 +- .pik/install.sh | 2 ++ .pik/man.sh | 6 ++++ README.md | 7 +++++ completion/completion.go | 16 +++++----- describe/describe.go | 6 ++-- help/help.go | 13 -------- help/help.txt | 35 --------------------- indexers/pikdex/index.go | 8 +++-- indexers/pikdex/meta.go | 43 +++++++++++++++++++------- indexers/pikdex/meta_test.go | 39 ++++++++++++++++++++++++ main.go | 72 ++++++++++++++++++++------------------------ man/data.go | 43 ++++++++++++++++++++++++++ man/help.txt | 37 +++++++++++++++++++++++ man/man.go | 50 ++++++++++++++++++++++++++++++ man/man_test.go | 18 +++++++++++ man/templates/pik.1.man.tmpl | 31 +++++++++++++++++++ man/version.txt | 1 + model/new.go | 5 +-- model/source.go | 5 +-- modes.go | 58 +++++++++++++++++------------------ order/order.go | 2 +- run/edit.go | 18 +++++++++-- spool/exit.go | 60 ++++++++++++++++++++++++++++++++++++ spool/spool.go | 23 ++++++++++++++ version.txt | 1 - 26 files changed, 450 insertions(+), 152 deletions(-) create mode 100644 .pik/man.sh delete mode 100644 help/help.go delete mode 100644 help/help.txt create mode 100644 indexers/pikdex/meta_test.go create mode 100644 man/data.go create mode 100644 man/help.txt create mode 100644 man/man.go create mode 100644 man/man_test.go create mode 100644 man/templates/pik.1.man.tmpl create mode 100644 man/version.txt create mode 100644 spool/exit.go delete mode 100644 version.txt diff --git a/.gitignore b/.gitignore index cdbeccf..6ba5589 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ web/coverage.html coverage.out # build artifact -pik \ No newline at end of file +pik +out \ No newline at end of file diff --git a/.pik/install.sh b/.pik/install.sh index 0b23681..82efd4a 100644 --- a/.pik/install.sh +++ b/.pik/install.sh @@ -17,4 +17,6 @@ if [[ $PATH != *"$DEST_FOLDER"* ]] ; then exit 1 fi +bash .pik/man.sh + echo "congratulations! You are now using pik $(pik --version)" \ No newline at end of file diff --git a/.pik/man.sh b/.pik/man.sh new file mode 100644 index 0000000..2066c5c --- /dev/null +++ b/.pik/man.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +MANDIR="$HOME/.local/share/man/man1" +mkdir -p "$MANDIR" +go run ./man +cp out/* "$MANDIR" \ No newline at end of file diff --git a/README.md b/README.md index d9ff372..18f866f 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,13 @@ attach to these features. * git pre-commit and pre-push triggers * linking sources together by `.include` and `.wants` files +## development + +### compiling manpages + +Manpages are not available as files in the repository because they have to be built. After running `pik man` they will +be available in the `out` folder and installed on your system. View [man.sh](.pik/man.sh) for more information. + ## thanks this repository lives [here](https://git.ewy.one/pik/) diff --git a/completion/completion.go b/completion/completion.go index 9d3ae72..0fbe877 100644 --- a/completion/completion.go +++ b/completion/completion.go @@ -35,23 +35,23 @@ var completionFileByShell = map[string]string{ var AlreadyInstalledError = errors.New("completion already installed") // Add finds the right file to append the completion code to and does that -func Add(shell string) error { +func Add(shell string) *spool.ExitCode { f := filepath.Join(paths.HomeDir.String(), completionFileByShell[shell]) content, err := os.ReadFile(f) if err != nil { - return err + return &spool.FatalReadFailure } if strings.Contains(string(content), strings.TrimSpace(completionCodeByShell[shell])) { - return AlreadyInstalledError + return &spool.CompletionAlreadyInstalledFailure } fd, err := os.OpenFile(f, os.O_APPEND|os.O_WRONLY, 0600) defer fd.Close() if err != nil { - return err + return &spool.FatalReadFailure } _, err = fd.Write([]byte(fmt.Sprintf(completionFormat, completionComment, completionCodeByShell[shell]))) if err != nil { - return err + return &spool.CompletionFailure } successMessage(shell, f) return nil @@ -63,7 +63,7 @@ func successMessage(shell string, file string) { // Echo prints the actual completion script // because it is baked in with the program it should always be version-appropriate -func Echo() error { - _, err := spool.Print("%s", completionCode) - return err +func Echo() *spool.ExitCode { + _, _ = spool.Print("%s", completionCode) + return nil } diff --git a/describe/describe.go b/describe/describe.go index 4c40529..0913409 100644 --- a/describe/describe.go +++ b/describe/describe.go @@ -8,7 +8,7 @@ import ( "strings" ) -var DescriptionPrefixes = []string{ +var CommentPrefixes = []string{ "#", "//", } @@ -49,7 +49,7 @@ func FromReader(reader io.Reader) (string, error) { } text = strings.TrimSpace(text) hasPrefix := false - for _, p := range DescriptionPrefixes { + for _, p := range CommentPrefixes { if strings.HasPrefix(text, p) { hasPrefix = true break @@ -58,7 +58,7 @@ func FromReader(reader io.Reader) (string, error) { if !hasPrefix { return "", nil } - for _, c := range DescriptionPrefixes { + for _, c := range CommentPrefixes { text = strings.TrimPrefix(text, c) text = strings.TrimSpace(text) } diff --git a/help/help.go b/help/help.go deleted file mode 100644 index c19ebeb..0000000 --- a/help/help.go +++ /dev/null @@ -1,13 +0,0 @@ -package help - -import ( - _ "embed" - "github.com/ewy1/pik/spool" -) - -//go:embed help.txt -var content string - -func Echo() { - _, _ = spool.Print("%v", content) -} diff --git a/help/help.txt b/help/help.txt deleted file mode 100644 index e5f4e4d..0000000 --- a/help/help.txt +++ /dev/null @@ -1,35 +0,0 @@ -Usage: pik [OPTION]... [SOURCE] [TARGET] [ARG]... - -If no arguments are provided, the TUI will open. - -Otherwise, pik will attempt to run the $TARGET script in $SOURCE. - -OPTIONS: - --help - print this information - -a, --all - in addition to crawling, also load all cached sources - -h, --here - run in the current working directory instead of the source folder - -@, --at [LOCATION] - run at $LOCATION instead of the source folder - -s, --single - skip triggers (pre, post, final targets) - -d, --dry - instead of running targets, echo their command - -r, --root - prefix target command with sudo - -y, --yes - assume yes for yes/no prompts - --env [ENVTYPE] [--env [OTHER]]... - load environment files that look like they belong to this category - -v, --version - print pik version and exit - -l, --list - list all available sources and targets instead of doing anything interactive - -i, -inline - disable TUI alt screen - --edit - open the target in $EDITOR instead of running it - --install-completion - install completion in your shell rc file (bash or zsh currently supported) diff --git a/indexers/pikdex/index.go b/indexers/pikdex/index.go index 70cfabb..1a9b38a 100644 --- a/indexers/pikdex/index.go +++ b/indexers/pikdex/index.go @@ -43,9 +43,11 @@ type pikdex struct { } type SourceData struct { - Aliases []string - Icon string - Path string + Aliases []string + Icon string + Path string + Wants []string + Includes []string } func (u *pikdex) Index(absPath string, f fs.FS, runners []model.Runner) ([]model.Target, error) { diff --git a/indexers/pikdex/meta.go b/indexers/pikdex/meta.go index 750ce47..ef2c029 100644 --- a/indexers/pikdex/meta.go +++ b/indexers/pikdex/meta.go @@ -2,24 +2,28 @@ package pikdex import ( "github.com/charmbracelet/lipgloss" + "github.com/ewy1/pik/describe" "strings" ) type MetaSetter func(s *SourceData, content string) var MetaFiles = map[string]MetaSetter{ + ".wants": func(s *SourceData, content string) { + s.Wants = contentLines(content) + }, + ".includes": func(s *SourceData, content string) { + s.Includes = contentLines(content) + }, ".alias": func(s *SourceData, content string) { - split := strings.Split(content, "\n") - s.Aliases = make([]string, 0, len(split)) - for _, line := range split { - stripped := strip(line) - if stripped != "" { - s.Aliases = append(s.Aliases, stripped) - } - } + s.Aliases = contentLines(content) }, ".icon": func(s *SourceData, content string) { - icon := strip(content) + lines := contentLines(content) + if len(lines) == 0 { + return + } + icon := lines[0] desiredWidth := lipgloss.Width(icon) diff := desiredWidth - len([]rune(icon)) icon += strings.Repeat(" ", diff) @@ -27,6 +31,23 @@ var MetaFiles = map[string]MetaSetter{ }, } -func strip(input string) string { - return strings.TrimSpace(input) +func contentLines(input string) []string { + result := make([]string, 0, len(input)) +nextLine: + for _, l := range strings.Split(input, "\n") { + l = strings.TrimSpace(l) + + if l == "" { + continue nextLine + } + + for _, c := range describe.CommentPrefixes { + if strings.HasPrefix(l, c) { + continue nextLine + } + } + + result = append(result, l) + } + return result } diff --git a/indexers/pikdex/meta_test.go b/indexers/pikdex/meta_test.go new file mode 100644 index 0000000..e04815f --- /dev/null +++ b/indexers/pikdex/meta_test.go @@ -0,0 +1,39 @@ +//go:build test + +package pikdex + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestContentLines(t *testing.T) { + in := `asdf +hjkl` + result := contentLines(in) + assert.Equal(t, "asdf", "hjkl", result) +} + +func TestContentLines_EmptyLine(t *testing.T) { + in := `asdf + +hjkl` + result := contentLines(in) + assert.Equal(t, "asdf", "hjkl", result) +} + +func TestContentLines_Empty(t *testing.T) { + in := `` + result := contentLines(in) + assert.Equal(t, nil, result) +} + +func TestContentLines_Comment(t *testing.T) { + in := ` +//aaa +asdf +# iuyyiuoui +hjkl` + result := contentLines(in) + assert.Equal(t, "asdf", "hjkl", result) +} diff --git a/main.go b/main.go index 25b570e..496d09b 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,11 @@ package main import ( _ "embed" - "errors" "fmt" "github.com/ewy1/pik/cache" "github.com/ewy1/pik/crawl" "github.com/ewy1/pik/flags" "github.com/ewy1/pik/git" - "github.com/ewy1/pik/help" "github.com/ewy1/pik/indexers/pikdex" "github.com/ewy1/pik/menu" "github.com/ewy1/pik/model" @@ -73,9 +71,6 @@ var ForceConfirm = false // used for stripping out results to prevent double-index var SourcesWithoutResults *cache.Cache -//go:embed version.txt -var version string - func main() { result := pik() if profileFd != nil { @@ -85,38 +80,36 @@ func main() { _, _ = spool.Warn("%v\n", err) } } - if result != 0 { - os.Exit(result) + if result.Value != 0 { + result.Exit() } } -func mode[T any](list ModeMap[T], fire func(mode T) error) *int { - err := list.Traverse(func(in T) error { +func mode[T any](list ModeMap[T], fire func(mode T) *spool.ExitCode) *spool.ExitCode { + code := list.Traverse(func(in T) *spool.ExitCode { return fire(in) }) - if errors.Is(err, Success) { - zero := 0 - return &zero - } else if err != nil { - _, _ = spool.Warn("%v\n", err) - one := 1 - return &one + if code != nil { + code.Exit() } return nil } -func pik() int { - pflag.Usage = help.Echo +func pik() spool.ExitCode { + + // initialize the flags outside the main method so it can re-run in case + // pik changes its own configuration at runtime + pflag.Usage = func() { _ = uninitializedModes[flags.Help]() } pflag.Parse() - code := mode(uninitializedModes, func(mode func() error) error { + code := mode(uninitializedModes, func(mode func() *spool.ExitCode) *spool.ExitCode { return mode() }) if code != nil { return *code } - code = mode(statelessModes, func(mode func() error) error { + code = mode(statelessModes, func(mode func() *spool.ExitCode) *spool.ExitCode { return mode() }) if code != nil { @@ -133,19 +126,24 @@ func pik() int { here, err := os.Getwd() if err != nil { _, _ = spool.Warn("%v\n", err) - return 1 + return spool.WorkingDirectoryFailure } locs := append(crawl.RichLocations(here), paths.System.String()) root, err := os.OpenRoot("/") - defer root.Close() + defer func(root *os.Root) { + err := root.Close() + if err != nil { + _, _ = spool.Warn("%v\n", err) + } + }(root) if root == nil { _, _ = spool.Warn("%v\n", err) - return 1 + return spool.OpenRootFailure } fs := root.FS() if err != nil { _, _ = spool.Warn("%v\n", err) - return 1 + return spool.RootFsFailure } var st *model.State var stateErrors []error @@ -167,7 +165,7 @@ func pik() int { c, err = cache.LoadFile(fs, paths.ContextsFile.String()[1:]) if err != nil { _, _ = spool.Warn("%v\n", err) - return 1 + return spool.CacheReadFailure } st, stateErrors = cache.LoadState(fs, c, indexers, runners) } @@ -175,7 +173,7 @@ func pik() int { _, _ = spool.Warn("%v\n", stateErrors) } - code = mode(statefulModes, func(mode func(st *model.State) error) error { + code = mode(statefulModes, func(mode func(st *model.State) *spool.ExitCode) *spool.ExitCode { return mode(st) }) if code != nil { @@ -191,7 +189,7 @@ func pik() int { md, err := menu.Show(st, hydrators) if err != nil { _, _ = spool.Warn("%v\n", err) - return 1 + return spool.HydrationFailure } cancelled = md.Cancel source, target := md.Result() @@ -214,35 +212,31 @@ func pik() int { // TODO: Move auto-all logic into Search? if !*flags.All && result.Target == nil && len(result.Args) > 0 && SourcesWithoutResults == nil && !ForceConfirm { ForceConfirm = true - if err != nil { - _, _ = spool.Warn("%v\n", err) - return 1 - } SourcesWithoutResults = c return pik() } if cancelled { - _, _ = spool.Warn("no target selected\n") - return 0 + _, _ = spool.Warn("operation cancelled\n") + return spool.Cancelled } if result.Target == nil { _, _ = spool.Warn("target not found\n") - return 1 + return spool.NotFoundFailure } if result.NeedsConfirmation || ForceConfirm { - _, _ = fmt.Fprintf(os.Stderr, "this target is out of tree.\n") + _, _ = fmt.Fprintf(os.Stderr, "this target is out of tree\n") if !menu.Confirm(os.Stdin, result.Source, result.Target, args...) { - return 0 + return spool.Cancelled } } if result.Overridden { _, _ = fmt.Fprintln(os.Stderr, menu.OverrideWarning(result.Target)) } - code = mode(selectionModes, func(mode func(st *model.State, src *model.Source, t model.Target) error) error { + code = mode(selectionModes, func(mode func(st *model.State, src *model.Source, t model.Target) *spool.ExitCode) *spool.ExitCode { return mode(st, result.Source, result.Target) }) if code != nil { @@ -253,8 +247,8 @@ func pik() int { if err != nil { _, _ = spool.Warn("%v\n", err) - return 1 + return spool.MenuFailure } - return 0 + return spool.Success } diff --git a/man/data.go b/man/data.go new file mode 100644 index 0000000..322da8f --- /dev/null +++ b/man/data.go @@ -0,0 +1,43 @@ +package main + +import ( + _ "embed" + _ "github.com/ewy1/pik/flags" + "github.com/ewy1/pik/spool" + "github.com/spf13/pflag" + "runtime" + "runtime/debug" + "time" +) + +//go:embed version.txt +var version string + +type ManData struct { + Flags []pflag.Flag + Now string + Revision string + Runtime string + Version string + ExitCodes map[int]*spool.ExitCode +} + +func NewData() ManData { + var flags []pflag.Flag + pflag.Parse() + pflag.VisitAll(func(flag *pflag.Flag) { + flags = append(flags, *flag) + }) + info, ok := debug.ReadBuildInfo() + if !ok { + _, _ = spool.Panic(spool.NoDebugInfo, "could not read debug info\n") + } + return ManData{ + Flags: flags, + Now: time.Now().Format(time.DateTime), + Runtime: runtime.Version(), + Version: version, + Revision: info.Main.Version, + ExitCodes: spool.CodeMap, + } +} diff --git a/man/help.txt b/man/help.txt new file mode 100644 index 0000000..8c7e943 --- /dev/null +++ b/man/help.txt @@ -0,0 +1,37 @@ +Usage: pik [OPTION]... [SOURCE] [TARGET] [ARG]... + +This help content is being replaced with manual pages. To view them, try `man pik`. + +If no arguments are provided, the TUI will open. + +Otherwise, pik will attempt to run the $TARGET script in $SOURCE. + +OPTIONS: + --help + print this information + -a, --all + in addition to crawling, also load all cached sources + -h, --here + run in the current working directory instead of the source folder + -@, --at [LOCATION] + run at $LOCATION instead of the source folder + -s, --single + skip triggers (pre, post, final targets) + -d, --dry + instead of running targets, echo their command + -r, --root + prefix target command with sudo + -y, --yes + assume yes for yes/no prompts + --env [ENVTYPE] [--env [OTHER]]... + load environment files that look like they belong to this category + -v, --version + print pik version and exit + -l, --list + list all available sources and targets instead of doing anything interactive + -i, -inline + disable TUI alt screen + --edit + open the target in $EDITOR instead of running it + --install-completion + install completion in your shell rc file (bash or zsh currently supported) diff --git a/man/man.go b/man/man.go new file mode 100644 index 0000000..49018ed --- /dev/null +++ b/man/man.go @@ -0,0 +1,50 @@ +package main + +import ( + "embed" + "github.com/ewy1/pik/spool" + "github.com/spf13/pflag" + "os" + "path/filepath" + "strings" + "text/template" +) + +//go:embed templates +var templates embed.FS + +var ManOutput = pflag.String(manFlagName, "out", "directory to write man pages to (gets created)") + +const manFlagName = "man-output" +const templateDir = "templates" +const manExtension = ".man" +const templateExtension = ".tmpl" + +func main() { + pflag.Parse() + tmpl, err := template.ParseFS(templates, filepath.Join(templateDir, "*")) + if err != nil { + _, _ = spool.Panic(spool.ManFailure, "%v\n", err) + return + } + err = os.MkdirAll(*ManOutput, os.ModePerm) + if err != nil { + _, _ = spool.Panic(spool.ManFailure, "%v\n", err) + } + d := NewData() + for _, t := range tmpl.Templates() { + if !strings.HasSuffix(t.Name(), manExtension+templateExtension) { + continue + } + + resultFile, err := os.OpenFile(filepath.Join(*ManOutput, strings.TrimSuffix(strings.TrimSuffix(t.Name(), templateExtension), manExtension)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + _, _ = spool.Panic(spool.ManFailure, "%v\n", err) + } + + err = t.Execute(resultFile, d) + if err != nil { + _, _ = spool.Panic(spool.ManFailure, "%v\n", err) + } + } +} diff --git a/man/man_test.go b/man/man_test.go new file mode 100644 index 0000000..4b48c0f --- /dev/null +++ b/man/man_test.go @@ -0,0 +1,18 @@ +//go:build test + +package main + +import ( + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "path/filepath" + "testing" +) + +func TestMan(t *testing.T) { + d := t.TempDir() + err := pflag.Set(manFlagName, d) + assert.NoError(t, err) + assert.NotPanics(t, main) + assert.FileExists(t, filepath.Join(d, "pik.1.man")) +} diff --git a/man/templates/pik.1.man.tmpl b/man/templates/pik.1.man.tmpl new file mode 100644 index 0000000..53f1cfd --- /dev/null +++ b/man/templates/pik.1.man.tmpl @@ -0,0 +1,31 @@ +{{- /*gotype: github.com/ewy1/pik/man.ManData*/ -}} +.TH PIK 1 {{.Version}} {{.Runtime}} +.sh NAME +pik \- file based task runner +.SH SYNOPSIS +.B pik +[\fIOPTION\fR]... [[\fISOURCE\fR] [\fITARGET\fR]] [\fIARG\fR]... +.SH DESCRIPTION +.B pik +executes scripts from the .pik folder or external runners. +.SH EXAMPLES +Given a .pik folder in your current working directory containing "script.sh", calling +.B pik script +will start that script. +.PP To start a script from another location, pass that as the SOURCE before the TARGET. +Example: +.B pik project build +will start ../../.pik/build.py if it exists. +.SH OPTIONS +{{ range .Flags -}} +.TP +.BR \-\-{{.Name }} {{ if .Shorthand }}, \-{{ .Shorthand }}{{ end }} = {{ .Value.Type }} {{ if .DefValue }} ({{.DefValue}}) {{- end }} +{{ .Usage }} +{{ end }} +.SH EXIT CODES +If the target runs and returns a non-zero error code, we pass that forward instead. +{{ range .ExitCodes -}} +.TP +.BR {{ .Value }} +{{ .Message }} +{{ end }} \ No newline at end of file diff --git a/man/version.txt b/man/version.txt new file mode 100644 index 0000000..3b2e7a0 --- /dev/null +++ b/man/version.txt @@ -0,0 +1 @@ +0.0.0-dev \ No newline at end of file diff --git a/model/new.go b/model/new.go index e83c314..94e19a8 100644 --- a/model/new.go +++ b/model/new.go @@ -21,8 +21,9 @@ func NewState(rootFs fs.FS, locations []string, indexers []Indexer, runners []Ru wg.Go(func() { _, dirName := filepath.Split(strings.TrimSuffix(loc, "/")) src := &Source{ - Path: loc, - Identity: identity.New(dirName), + Path: loc, + Identity: identity.New(dirName), + Whitelists: make(map[string][]string), } sources[i] = src loc = strings.TrimSuffix(loc, "/") diff --git a/model/source.go b/model/source.go index 60f4c3d..246929e 100644 --- a/model/source.go +++ b/model/source.go @@ -11,8 +11,9 @@ import ( type Source struct { identity.Identity Tags - Path string - Targets []Target + Path string + Targets []Target + Whitelists map[string][]string } // HydratedSource is a Source with additional hydration diff --git a/modes.go b/modes.go index ec599a6..b8da9b0 100644 --- a/modes.go +++ b/modes.go @@ -1,10 +1,10 @@ package main import ( + _ "embed" "errors" "github.com/ewy1/pik/completion" "github.com/ewy1/pik/flags" - "github.com/ewy1/pik/help" "github.com/ewy1/pik/model" "github.com/ewy1/pik/paths" "github.com/ewy1/pik/run" @@ -22,77 +22,75 @@ type ModeMap[T any] map[*bool]T var Continue = errors.New("not an error; continue flow") var Success = errors.New("not an error; finished operations") +//go:embed man/help.txt +var help string + +//go:embed man/version.txt +var version string + // Traverse checks the entries of the map. If any flags are set on, // pik that mode. If Continue is returned, it's non-exclusive. Otherwise, // we quit after one mode. // // `then` should simply be the method call (necessary due to generics) -func (m ModeMap[T]) Traverse(then func(in T) error) error { +func (m ModeMap[T]) Traverse(then func(in T) *spool.ExitCode) *spool.ExitCode { for enabled, mode := range m { if !*enabled { continue } - err := then(mode) - if errors.Is(err, Continue) { - continue - } else if err != nil { - return err - } - return Success + return then(mode) } return nil } var profileFd *os.File -var UnknownShellError = errors.New("$SHELL not set or empty") - // uninitializedModes are modes which can run before the program runs initializers -var uninitializedModes = ModeMap[func() error]{ - flags.Version: func() error { - _, err := spool.Print("%s\n", version) - return err +var uninitializedModes = ModeMap[func() *spool.ExitCode]{ + flags.Version: func() *spool.ExitCode { + _, _ = spool.Print("%s\n", version) + return &spool.Success }, - flags.Completion: func() error { + flags.Completion: func() *spool.ExitCode { return completion.Echo() }, - flags.Help: func() error { - help.Echo() + flags.Help: func() *spool.ExitCode { + _, _ = spool.Print("%s\n", help) return nil }, } // statelessModes are program modes which do not require state to operate. // like --version and --completion -var statelessModes = ModeMap[func() error]{ - flags.InstallCompletion: func() error { +var statelessModes = ModeMap[func() *spool.ExitCode]{ + flags.InstallCompletion: func() *spool.ExitCode { sh := os.Getenv("SHELL") if sh == "" { - return UnknownShellError + return &spool.UnknownShellFailure } _, sh = filepath.Split(sh) return completion.Add(sh) }, - flags.Profile: func() error { + flags.Profile: func() *spool.ExitCode { fd, err := os.Create("pik-profile.out") if err != nil { - return err + return &spool.FatalWriteFailure } runtime.SetCPUProfileRate(1000) err = pprof.StartCPUProfile(profileFd) if err != nil { - return err + return &spool.ProfilingFailure } profileFd = fd - return Continue + return nil }, } var NoTargetsError = errors.New("no targets or sources to list") // statefulModes are program modes which require a built state to be executed -var statefulModes = ModeMap[func(st *model.State) error]{ - flags.List: func(st *model.State) error { +var statefulModes = ModeMap[func(st *model.State) *spool.ExitCode]{ + flags.List: func(st *model.State) *spool.ExitCode { count := 0 for _, s := range st.Sources { count++ @@ -104,15 +102,15 @@ var statefulModes = ModeMap[func(st *model.State) error]{ } if count == 0 { - return NoTargetsError + return &spool.NoTargetsFailure } return nil }, } // selectionModes are program modes which require a selected target, through menu or args -var selectionModes = ModeMap[func(st *model.State, src *model.Source, t model.Target) error]{ - flags.Edit: func(st *model.State, src *model.Source, t model.Target) error { +var selectionModes = ModeMap[func(st *model.State, src *model.Source, t model.Target) *spool.ExitCode]{ + flags.Edit: func(st *model.State, src *model.Source, t model.Target) *spool.ExitCode { return run.Edit(t, src) }, } diff --git a/order/order.go b/order/order.go index 41bbccd..155f067 100644 --- a/order/order.go +++ b/order/order.go @@ -41,7 +41,7 @@ func FromReader(r io.Reader) (Order, error) { continue } - for _, p := range describe.DescriptionPrefixes { + for _, p := range describe.CommentPrefixes { if strings.HasPrefix(line, p) { continue } diff --git a/run/edit.go b/run/edit.go index f4742ff..47d6362 100644 --- a/run/edit.go +++ b/run/edit.go @@ -3,21 +3,33 @@ package run import ( "errors" "github.com/ewy1/pik/model" + "github.com/ewy1/pik/spool" "os" "os/exec" ) var NoEditorError = errors.New("$EDITOR not set") -func Edit(t model.Target, src *model.Source) error { +func Edit(t model.Target, src *model.Source) *spool.ExitCode { editor := os.Getenv("EDITOR") if editor == "" { - return NoEditorError + return &spool.NoEditorFailure } cmd := exec.Command(editor, t.File(src)) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = src.Path - return cmd.Run() + err := cmd.Run() + code := 0 + msg := "" + var ex *exec.ExitError + if errors.As(err, &ex) { + code = ex.ExitCode() + msg = ex.String() + } + return &spool.ExitCode{ + Value: code, + Message: msg, + } } diff --git a/spool/exit.go b/spool/exit.go new file mode 100644 index 0000000..efe9d4e --- /dev/null +++ b/spool/exit.go @@ -0,0 +1,60 @@ +package spool + +import ( + "github.com/ewy1/pik/paths" + "os" +) + +type ExitCode struct { + Value int + Message string +} + +var ( + Success = New(0, "succesful") + Cancelled = New(0, "operation cancelled by user") + NotFoundFailure = Cancelled.Next("target not found") + FatalReadFailure = NotFoundFailure.Next("fatal failure during initialization") + FatalWriteFailure = FatalReadFailure.Next("fatal failure during file write") + WorkingDirectoryFailure = FatalWriteFailure.Next("could not get current working directory") + OpenRootFailure = WorkingDirectoryFailure.Next("failed to init root directory") + RootFsFailure = OpenRootFailure.Next("failed to make fs from root") + CacheReadFailure = RootFsFailure.Next("failed to read cache (from " + paths.ContextsFile.String()) + HydrationFailure = CacheReadFailure.Next("a hydrator failed") + MenuFailure = HydrationFailure.Next("error during menu") + NoTargetsFailure = MenuFailure.Next("no targets found") + NoEditorFailure = NoTargetsFailure.Next("$EDITOR not set") + NoDebugInfo = NoEditorFailure.Next("could not read debug info") + ManFailure = New(120, "failure to generate manual pages") + UnknownShellFailure = New(110, "unable to detect shell type through $SHELL") + CompletionAlreadyInstalledFailure = UnknownShellFailure.Next("completion seems already installed") + CompletionFailure = CompletionAlreadyInstalledFailure.Next("failed to install completion") + ProfilingFailure = CompletionFailure.Next("failed to initialize profiler") +) + +var Codes []ExitCode +var CodeMap = make(map[int]*ExitCode) + +func New(num int, message string) ExitCode { + if CodeMap[num] != nil && num != 0 { + _, _ = Warn("redefined error code: %v", num) + } + c := ExitCode{ + Value: num, + Message: message, + } + Codes = append(Codes, c) + CodeMap[num] = &c + return c +} + +func (e ExitCode) Exit() { + os.Exit(e.Value) +} + +func (e ExitCode) Next(message string) ExitCode { + return New( + e.Value+1, + message, + ) +} diff --git a/spool/spool.go b/spool/spool.go index e7b13e9..4335d50 100644 --- a/spool/spool.go +++ b/spool/spool.go @@ -3,6 +3,9 @@ package spool import ( "fmt" "os" + "reflect" + "runtime/debug" + "strings" ) var ( @@ -10,6 +13,10 @@ var ( Stdout = os.Stdout ) +type empty struct{} + +const PanicErrorCode = 8 + var Print = func(format string, values ...any) (any, error) { return fmt.Fprintf(Stdout, format, values...) } @@ -17,3 +24,19 @@ var Print = func(format string, values ...any) (any, error) { var Warn = func(format string, values ...any) (any, error) { return fmt.Fprintf(Stderr, format, values...) } + +var Panic = func(code ExitCode, format string, values ...any) (any, error) { + pkg := reflect.TypeOf(empty{}).PkgPath() + v, err := fmt.Fprintf(Stderr, format, values...) + st := strings.Split(string(debug.Stack()), "\n") + for i, l := range st { + if strings.Contains(l, pkg) { + st = st[i:] + break + } + } + + _, _ = Warn("%s\n", strings.Join(st, "\n")) + os.Exit(PanicErrorCode) + return v, err +} diff --git a/version.txt b/version.txt deleted file mode 100644 index 3b2e7a0..0000000 --- a/version.txt +++ /dev/null @@ -1 +0,0 @@ -0.0.0-dev \ No newline at end of file -- cgit v1.3.1