viye/internal/viye/viye.go (view raw)
| 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 | } |