|
1
|
package core |
|
2
|
|
|
3
|
import ( |
|
4
|
"fmt" |
|
5
|
"io" |
|
6
|
"os" |
|
7
|
"strings" |
|
8
|
) |
|
9
|
|
|
10
|
type Context struct { |
|
11
|
Path []string // ancestor chain [root, ..., target] |
|
12
|
Dir string // working directory |
|
13
|
|
|
14
|
dispatch func(*Context) (string, error) // set by [Viye.dispatch] for tool chaining |
|
15
|
} |
|
16
|
|
|
17
|
// Next continues dispatch with currect [Context] state. |
|
18
|
// Call after consuming .Path[0] and updating .Dir. |
|
19
|
func (c *Context) Next() (string, error) { return c.dispatch(c) } |
|
20
|
|
|
21
|
type Tool interface { |
|
22
|
Name() string |
|
23
|
Match(*Context) bool |
|
24
|
Execute(*Context) (string, error) |
|
25
|
} |
|
26
|
|
|
27
|
type Viye struct { |
|
28
|
tools []Tool |
|
29
|
} |
|
30
|
|
|
31
|
func New() *Viye { return &Viye{} } |
|
32
|
|
|
33
|
func (v *Viye) Register(tool Tool) { |
|
34
|
v.tools = append(v.tools, tool) |
|
35
|
} |
|
36
|
|
|
37
|
func (v *Viye) Run(out io.Writer, args []string) error { |
|
38
|
if len(args) <= 1 { |
|
39
|
return nil |
|
40
|
} |
|
41
|
if args[1] == "--help" { |
|
42
|
return v.showHelp(out) |
|
43
|
} |
|
44
|
|
|
45
|
path := splitArgs(args[1:]) |
|
46
|
if len(path) == 0 { |
|
47
|
os.Exit(0) |
|
48
|
} |
|
49
|
|
|
50
|
res, err := v.dispatch(&Context{ |
|
51
|
Path: path, |
|
52
|
Dir: ".", // TODO: get cwd |
|
53
|
}) |
|
54
|
if err != nil { |
|
55
|
return err |
|
56
|
} |
|
57
|
|
|
58
|
_, err = fmt.Fprint(out, res) |
|
59
|
return err |
|
60
|
} |
|
61
|
|
|
62
|
func (v *Viye) showHelp(out io.Writer) error { |
|
63
|
fmt.Fprintln(out, "Available tools:") |
|
64
|
for _, tool := range v.tools { |
|
65
|
fmt.Fprintf(out, " - %s\n", tool.Name()) |
|
66
|
} |
|
67
|
return nil |
|
68
|
} |
|
69
|
|
|
70
|
// dispatch executes the path against registered tools, first-match-wins |
|
71
|
func (v *Viye) dispatch(ctx *Context) (string, error) { |
|
72
|
if len(ctx.Path) == 0 { |
|
73
|
return "", nil |
|
74
|
} |
|
75
|
ctx.dispatch = v.dispatch |
|
76
|
for _, tool := range v.tools { |
|
77
|
if tool.Match(ctx) { |
|
78
|
return tool.Execute(ctx) |
|
79
|
} |
|
80
|
} |
|
81
|
return "", nil |
|
82
|
} |
|
83
|
|
|
84
|
// splitArgs builds path by splitting each arg on "/" |
|
85
|
// for absolute paths(/...), leading / is preserved as part of first component. |
|
86
|
// for home paths(~...), ~ is the first comonents |
|
87
|
// urls are kept as single component |
|
88
|
func splitArgs(args []string) []string { |
|
89
|
var path []string |
|
90
|
for _, arg := range args { |
|
91
|
path = append(path, splitArg(arg)...) |
|
92
|
} |
|
93
|
return path |
|
94
|
} |
|
95
|
|
|
96
|
func splitArg(arg string) []string { |
|
97
|
switch { |
|
98
|
case strings.HasPrefix(arg, "/"): |
|
99
|
parts := strings.Split(arg, "/") |
|
100
|
// parts[0] is always "" for absolute paths |
|
101
|
i := 0 |
|
102
|
for i < len(parts) && parts[i] == "" { |
|
103
|
i++ |
|
104
|
} |
|
105
|
if i >= len(parts) { |
|
106
|
return []string{"/"} |
|
107
|
} |
|
108
|
// parts[i] is the first non-empty segment → attach it to "/" |
|
109
|
result := []string{"/" + parts[i]} |
|
110
|
result = append(result, parts[i+1:]...) |
|
111
|
return result |
|
112
|
case strings.HasPrefix(arg, "~"): |
|
113
|
return strings.Split(arg, "/") |
|
114
|
case strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://"): |
|
115
|
return []string{arg} |
|
116
|
default: |
|
117
|
return strings.Split(arg, "/") |
|
118
|
} |
|
119
|
} |