all repos

viye @ 503d76b666c977833e7d0eb48f17e4312a77770c

my shot at reimplementing xiki

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

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