summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorewy <ewy0@protonmail.com>2026-06-01 18:47:44 +0200
committerewy <ewy0@protonmail.com>2026-06-01 18:47:44 +0200
commit46d032cd21b0e8e2c94a32333d3805ec76980cca (patch)
treefbb8aed5c5a501aca1309f62a9d4440ca949ce4c
parent7585a488b7b1e1812f7ebf50107139e2fd65f035 (diff)
add man(1) generation
-rw-r--r--.gitignore3
-rw-r--r--.pik/install.sh2
-rw-r--r--.pik/man.sh6
-rw-r--r--README.md7
-rw-r--r--completion/completion.go16
-rw-r--r--describe/describe.go6
-rw-r--r--help/help.go13
-rw-r--r--indexers/pikdex/index.go8
-rw-r--r--indexers/pikdex/meta.go43
-rw-r--r--indexers/pikdex/meta_test.go39
-rw-r--r--main.go72
-rw-r--r--man/data.go43
-rw-r--r--man/help.txt (renamed from help/help.txt)2
-rw-r--r--man/man.go50
-rw-r--r--man/man_test.go18
-rw-r--r--man/templates/pik.1.man.tmpl31
-rw-r--r--man/version.txt (renamed from version.txt)0
-rw-r--r--model/new.go5
-rw-r--r--model/source.go5
-rw-r--r--modes.go58
-rw-r--r--order/order.go2
-rw-r--r--run/edit.go18
-rw-r--r--spool/exit.go60
-rw-r--r--spool/spool.go23
24 files changed, 414 insertions, 116 deletions
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/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/help/help.txt b/man/help.txt
index e5f4e4d..8c7e943 100644
--- a/help/help.txt
+++ b/man/help.txt
@@ -1,5 +1,7 @@
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.
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/version.txt b/man/version.txt
index 3b2e7a0..3b2e7a0 100644
--- a/version.txt
+++ b/man/version.txt
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
+}