all repos

viye @ 99e30d0

my shot at reimplementing xiki

viye/internal/viye/viye.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
refactor, 1 month ago
1
package viye
2
3
import (
4
	"errors"
5
	"fmt"
6
	"io"
7
	"os"
8
	"strings"
9
)
10
11
const Version = "0.0.1-alpha"
12
13
var ErrPlumbingNotFound = errors.New("tool not found")
14
15
type Context struct {
16
	Dir string // working directory
17
18
	Cmd  string   // first word of the leaf path component
19
	Args []string // remaining words of the leaf path component
20
	Path []string // ancestor chain [root, ..., target]
21
	Body []string // body lines from : children (edited data for two-phase execution)
22
23
	dispatch func(*Context) (string, error) // set by [Viye.dispatch] for tool chaining
24
}
25
26
// Next continues dispatch with currect [Context] state.
27
func (c *Context) Next() (string, error) { return c.dispatch(c) }
28
29
type Tool interface {
30
	Name() string
31
	Match(*Context) bool
32
	Execute(*Context) (string, error)
33
}
34
35
type Viye struct {
36
	tools []Tool
37
}
38
39
func New() *Viye { return &Viye{} }
40
41
func (v *Viye) Register(tool Tool) {
42
	v.tools = append(v.tools, tool)
43
}
44
45
func (v *Viye) Run(out io.Writer, args []string) error {
46
	if len(args) <= 1 {
47
		return nil
48
	}
49
	if args[1] == "help" {
50
		return v.showHelp(out)
51
	}
52
	if args[1] == "version" {
53
		return v.showVersion(out)
54
	}
55
56
	path, body := splitArgs(args[1:])
57
	if len(path) == 0 {
58
		os.Exit(0)
59
	}
60
61
	res, err := v.dispatch(&Context{
62
		Path: path,
63
		Dir:  mustGetCwd(),
64
		Body: body,
65
	})
66
	if err != nil {
67
		return err
68
	}
69
70
	_, err = fmt.Fprint(out, res)
71
	return err
72
}
73
74
func (v *Viye) showHelp(out io.Writer) error {
75
	fmt.Fprintln(out, "Available tools:")
76
	for _, tool := range v.tools {
77
		fmt.Fprintf(out, "- %s\n", tool.Name())
78
	}
79
	return nil
80
}
81
82
func (v *Viye) showVersion(out io.Writer) error {
83
	fmt.Fprint(out, "| viye version: "+Version)
84
	return nil
85
}
86
87
// dispatch executes the path against registered tools, first-match-wins
88
func (v *Viye) dispatch(ctx *Context) (string, error) {
89
	if len(ctx.Path) == 0 {
90
		return "", nil
91
	}
92
	ctx.Cmd, ctx.Args = splitLeaf(ctx.Path[len(ctx.Path)-1])
93
	ctx.dispatch = v.dispatch
94
	for _, tool := range v.tools {
95
		if tool.Match(ctx) {
96
			return tool.Execute(ctx)
97
		}
98
	}
99
	return "", ErrPlumbingNotFound
100
}
101
102
// splitLeaf splits a leaf path component into command name and args.
103
// For "$ go run ." returns ("$", []string{"go", "run", "."}).
104
// For "ip" returns ("ip", nil).
105
func splitLeaf(leaf string) (cmd string, args []string) {
106
	parts := strings.Fields(leaf)
107
	if len(parts) == 0 {
108
		return "", nil
109
	}
110
	return parts[0], parts[1:]
111
}
112
113
// splitArgs builds path and body from args.
114
// Everything before "--" is the path (each arg is one component).
115
// Everything after "--" is the body (kept as-is, one element per line).
116
// If there's no "--", body is nil.
117
func splitArgs(args []string) (path []string, body []string) {
118
	sawSep := false
119
	for _, arg := range args {
120
		if !sawSep && arg == "--" {
121
			sawSep = true
122
			continue
123
		}
124
		if !sawSep {
125
			path = append(path, arg)
126
		} else {
127
			body = append(body, arg)
128
		}
129
	}
130
	return
131
}
132
133
func mustGetCwd() string {
134
	dir, err := os.Getwd()
135
	if err != nil {
136
		panic(fmt.Sprintf("failed to get cwd: %v", err))
137
	}
138
	return dir
139
}