all repos

viye @ fa602cd19cb3521c7731a187b6e70f3e1c8cdebf

my shot at reimplementing xiki

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
add neofetch, 2 months ago
1
package viye
2
3
import (
4
	"fmt"
5
	"io"
6
	"os"
7
	"strings"
8
9
	"github.com/olexsmir/viye/internal/version"
10
)
11
12
type Context struct {
13
	Path []string // ancestor chain [root, ..., target]
14
	Dir  string   // working directory
15
16
	Cmd  string   // first word of the leaf path component
17
	Args []string // remaining words of the leaf path component
18
	Body []string // body lines from : children (edited data for two-phase execution)
19
20
	dispatch func(*Context) (string, error) // set by [Viye.dispatch] for tool chaining
21
}
22
23
// Next continues dispatch with currect [Context] state.
24
// Call after consuming .Path[0] and updating .Dir.
25
func (c *Context) Next() (string, error) { return c.dispatch(c) }
26
27
type Tool interface {
28
	Name() string
29
	Match(*Context) bool
30
	Execute(*Context) (string, error)
31
}
32
33
type Viye struct {
34
	tools []Tool
35
}
36
37
func New() *Viye { return &Viye{} }
38
39
func (v *Viye) Register(tool Tool) {
40
	v.tools = append(v.tools, tool)
41
}
42
43
func (v *Viye) Run(out io.Writer, args []string) error {
44
	if len(args) <= 1 {
45
		return nil
46
	}
47
	if args[1] == "help" {
48
		return v.showHelp(out)
49
	}
50
	if args[1] == "--version" {
51
		fmt.Fprint(out, "| viye version: "+version.Version)
52
		return nil
53
	}
54
55
	path, body := splitArgs(args[1:])
56
	if len(path) == 0 {
57
		os.Exit(0)
58
	}
59
60
	cmd, cmdArgs := splitLeaf(path[len(path)-1])
61
	res, err := v.dispatch(&Context{
62
		Path: path,
63
		Dir:  mustGetCwd(),
64
		Cmd:  cmd,
65
		Args: cmdArgs,
66
		Body: body,
67
	})
68
	if err != nil {
69
		return err
70
	}
71
72
	_, err = fmt.Fprint(out, res)
73
	return err
74
}
75
76
func (v *Viye) showHelp(out io.Writer) error {
77
	fmt.Fprintln(out, "Available tools:")
78
	for _, tool := range v.tools {
79
		fmt.Fprintf(out, "- %s\n", tool.Name())
80
	}
81
	return nil
82
}
83
84
// dispatch executes the path against registered tools, first-match-wins
85
func (v *Viye) dispatch(ctx *Context) (string, error) {
86
	if len(ctx.Path) == 0 {
87
		return "", nil
88
	}
89
	ctx.dispatch = v.dispatch
90
	for _, tool := range v.tools {
91
		if tool.Match(ctx) {
92
			return tool.Execute(ctx)
93
		}
94
	}
95
	return "", nil
96
}
97
98
// splitLeaf splits a leaf path component into command name and args.
99
// For "mkdir dir" returns ("mkdir", []string{"dir"}).
100
// For "$ go run ." returns ("$", []string{"go", "run", "."}).
101
// For "ip" returns ("ip", nil).
102
func splitLeaf(leaf string) (cmd string, args []string) {
103
	parts := strings.Fields(leaf)
104
	if len(parts) == 0 {
105
		return "", nil
106
	}
107
	return parts[0], parts[1:]
108
}
109
110
// splitArgs builds path by splitting each arg on "/" and body on "--".
111
// Everything before "--" is the path (split into components).
112
// Everything after "--" is the body (kept as-is, one element per line).
113
// If there's no "--", body is nil.
114
// for absolute paths(/...), leading / is preserved as part of first component.
115
// for home paths(~...), ~ is the first comonents
116
// urls are kept as single component
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, splitArg(arg)...)
126
		} else {
127
			body = append(body, arg)
128
		}
129
	}
130
	return
131
}
132
133
func splitArg(arg string) []string {
134
	switch {
135
	case strings.HasPrefix(arg, "/"):
136
		parts := strings.Split(arg, "/")
137
		// parts[0] is always "" for absolute paths
138
		i := 0
139
		for i < len(parts) && parts[i] == "" {
140
			i++
141
		}
142
		if i >= len(parts) {
143
			return []string{"/"}
144
		}
145
		// parts[i] is the first non-empty segment → attach it to "/"
146
		result := []string{"/" + parts[i]}
147
		i++
148
		for ; i < len(parts); i++ {
149
			if parts[i] != "" {
150
				result = append(result, parts[i])
151
			}
152
		}
153
		return result
154
	case strings.HasPrefix(arg, "~"):
155
		parts := strings.Split(arg, "/")
156
		var result []string
157
		for _, p := range parts {
158
			if p != "" {
159
				result = append(result, p)
160
			}
161
		}
162
		return result
163
	case strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://"):
164
		return []string{arg}
165
	default:
166
		s := strings.TrimPrefix(arg, "./")
167
		if s != arg {
168
			if s == "" {
169
				return []string{"."}
170
			}
171
			// No "/" left to split on — keep original (e.g. "./..." stays "./...")
172
			if !strings.Contains(s, "/") {
173
				return []string{arg}
174
			}
175
			return splitArg(s)
176
		}
177
		parts := strings.Split(s, "/")
178
		var result []string
179
		for _, p := range parts {
180
			if p != "" {
181
				result = append(result, p)
182
			}
183
		}
184
		return result
185
	}
186
}
187
188
func mustGetCwd() string {
189
	dir, err := os.Getwd()
190
	if err != nil {
191
		panic(fmt.Sprintf("failed to get cwd: %v", err))
192
	}
193
	return dir
194
}