all repos

viye @ 66ef839e7fe6e1655d8036d32fce619faa7b1be0

my shot at reimplementing xiki

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

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