13 files changed,
113 insertions(+),
176 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-08-25 17:20:38 +0300
Authored at:
2026-08-25 12:17:48 +0300
Change ID:
zoroskmxlmlsttnxtzuzloquttsmvqyu
Parent:
00a2383
jump to
M
internal/plumbing/gobin/gobin.go
··· 2 2 3 3 import ( 4 4 "os/exec" 5 - "strings" 6 5 7 6 "github.com/olexsmir/viye/internal/viye" 8 7 ) ··· 22 21 23 22 type Tool struct{} 24 23 25 -func (Tool) Name() string { return "go" } 26 -func (Tool) Match(ctx *viye.Context) bool { 27 - return ctx.Cmd == "go" || (len(ctx.Path) > 0 && ctx.Path[0] == "go") 28 -} 29 - 30 -func (Tool) Execute(ctx *viye.Context) (string, error) { 31 - var subcmd string 32 - var args []string 33 - 34 - if ctx.Cmd == "go" { 35 - // cmd-based: 'go build ./...' → Cmd="go", Args=["build","./..."] 36 - if len(ctx.Args) == 0 { 37 - return menu, nil 38 - } 39 - subcmd = ctx.Args[0] 40 - args = ctx.Args[1:] 41 - } else { 42 - // path-based: join remaining path as a command line, then re-split 43 - if len(ctx.Path) < 2 { 44 - return menu, nil 45 - } 46 - parts := strings.Fields(strings.Join(ctx.Path[1:], " ")) 47 - if len(parts) == 0 { 48 - return menu, nil 49 - } 50 - subcmd = parts[0] 51 - args = parts[1:] 24 +func (Tool) Name() string { return "go" } 25 +func (Tool) Match(c *viye.Context) bool { return c.Cmd == "go" } 26 +func (Tool) Execute(c *viye.Context) (string, error) { 27 + if len(c.Args) == 0 { 28 + return menu, nil 52 29 } 53 30 54 - switch subcmd { 31 + args := c.Args[1:] 32 + switch c.Args[0] { 55 33 case "mod": 56 34 if len(args) == 0 { 57 35 return modMenu, nil ··· 61 39 return menu, nil 62 40 } 63 41 64 - return runGo(ctx.Dir, subcmd, args...) 42 + return runGo(c.Dir, c.Args[0], args...) 65 43 } 66 44 67 45 func runGo(dir, subcmd string, args ...string) (string, error) {
M
internal/plumbing/gobin/gobin_test.go
··· 4 4 "testing" 5 5 6 6 "github.com/olexsmir/viye/internal/viye" 7 - 8 7 "olexsmir.xyz/x/is" 9 8 ) 10 9 11 10 func TestMatch(t *testing.T) { 12 11 tests := []struct { 13 - path []string 12 + cmd string 13 + args []string 14 14 want bool 15 15 }{ 16 - {[]string{"go"}, true}, 17 - {[]string{"go", "build"}, true}, 18 - {[]string{"go", "test", "./..."}, true}, 19 - {[]string{"other"}, false}, 20 - {[]string{}, false}, 16 + {"go", nil, true}, 17 + {"go", []string{"test"}, true}, 18 + {"go", []string{"test", "./..."}, true}, 19 + {"other", nil, false}, 20 + {"", nil, false}, 21 21 } 22 22 for _, tt := range tests { 23 - got := (Tool{}).Match(&viye.Context{Path: tt.path}) 23 + got := (Tool{}).Match(&viye.Context{Cmd: tt.cmd, Args: tt.args}) 24 24 is.Equal(t, tt.want, got) 25 25 } 26 26 }
M
internal/plumbing/ip/ip.go
··· 15 15 16 16 type Tool struct{} 17 17 18 -func (Tool) Name() string { return "ip" } 19 -func (Tool) Match(ctx *viye.Context) bool { return len(ctx.Path) == 1 && ctx.Path[0] == "ip" } 20 -func (Tool) Execute(ctx *viye.Context) (string, error) { 18 +func (Tool) Name() string { return "ip" } 19 +func (Tool) Match(c *viye.Context) bool { return c.Cmd == "ip" } 20 +func (Tool) Execute(c *viye.Context) (string, error) { 21 21 pip, perr := getPublicIP() 22 22 lip, lerr := getLocalIP() 23 23 if perr != nil && lerr != nil {
M
internal/plumbing/json/json.go
··· 10 10 11 11 type Tool struct{} 12 12 13 -func (Tool) Name() string { return "json" } 14 -func (Tool) Match(ctx *viye.Context) bool { return ctx.Cmd == "json" } 15 -func (Tool) Execute(ctx *viye.Context) (string, error) { 16 - if len(ctx.Body) == 0 { 13 +func (Tool) Name() string { return "json" } 14 +func (Tool) Match(c *viye.Context) bool { return c.Cmd == "json" } 15 +func (Tool) Execute(c *viye.Context) (string, error) { 16 + if len(c.Body) == 0 { 17 17 return ": name: your name\n" + 18 18 ": age: 0\n", nil 19 19 } 20 20 21 21 m := make(map[string]string) 22 - for _, line := range ctx.Body { 22 + for _, line := range c.Body { 23 23 line = strings.TrimPrefix(line, ": ") 24 - col := strings.Index(line, ":") 25 - if col < 0 { 24 + before, after, ok := strings.Cut(line, ":") 25 + if !ok { 26 26 continue 27 27 } 28 28 29 - key := strings.TrimSpace(line[:col]) 30 - val := strings.TrimSpace(line[col+1:]) 29 + key := strings.TrimSpace(before) 30 + val := strings.TrimSpace(after) 31 31 if key != "" { 32 32 m[key] = val 33 33 } 34 34 } 35 35 36 - b, err := json.Marshal(m) 36 + mashaled, err := json.Marshal(m) 37 37 if err != nil { 38 38 return "", fmt.Errorf("demo: %w", err) 39 39 } 40 - return fmt.Sprintf("| %s\n", string(b)), nil 40 + return fmt.Sprintf("| %s\n", string(mashaled)), nil 41 41 }
M
internal/plumbing/neofetch/neofetch.go
··· 5 5 "strings" 6 6 7 7 "github.com/olexsmir/viye/internal/osutil" 8 - "github.com/olexsmir/viye/internal/version" 9 8 "github.com/olexsmir/viye/internal/viye" 10 9 ) 11 10 ··· 24 23 _, _ = fmt.Fprintf(&buf, "| OS: %s (%s)\n", osname, kernel) 25 24 _, _ = fmt.Fprintf(&buf, "| SYS: %s · %s\n", cpu, mem) 26 25 _, _ = fmt.Fprintf(&buf, "| Uptime: %s\n", uptime) 27 - _, _ = fmt.Fprintf(&buf, "| Viye: %s\n", version.Version) 26 + _, _ = fmt.Fprintf(&buf, "| Viye: %s\n", viye.Version) 28 27 return buf.String(), nil 29 28 }
M
internal/plumbing/shell/shell.go
··· 16 16 17 17 type Tool struct{} 18 18 19 -func (Tool) Name() string { return "shell($, $$, kill, mkdir)" } 20 -func (Tool) Match(ctx *viye.Context) bool { 21 - return isCmd(ctx) || isBGCmd(ctx) || isKill(ctx) || isMkdir(ctx) 22 -} 23 - 19 +func (Tool) Name() string { return "shell($, $$, kill, mkdir)" } 20 +func (Tool) Match(c *viye.Context) bool { return isCmd(c) || isBGCmd(c) || isKill(c) || isMkdir(c) } 24 21 func (Tool) Execute(c *viye.Context) (string, error) { 25 22 switch { 26 23 case isCmd(c):
M
internal/plumbing/shell/shell_test.go
··· 12 12 13 13 func TestMatch(t *testing.T) { 14 14 tests := []struct { 15 - path, cmd string 16 - args []string 17 - want bool 15 + cmd string 16 + args []string 17 + want bool 18 18 }{ 19 - {"$", "$", nil, true}, 20 - {"$ go run .", "$", []string{"go", "run", "."}, true}, 21 - {"$$", "$$", nil, true}, 22 - {"$$ sleep 10", "$$", []string{"sleep", "10"}, true}, 23 - {"kill 1234", "kill", []string{"1234"}, true}, 24 - {"kill abc", "kill", []string{"abc"}, true}, 25 - {"mkdir dir", "mkdir", []string{"dir"}, true}, 26 - {"mkdir dir1 dir2", "mkdir", []string{"dir1", "dir2"}, true}, 27 - {"", "", nil, false}, 28 - {"ip", "ip", nil, false}, 29 - {"kill", "kill", nil, false}, 30 - {"kill 1 2", "kill", []string{"1", "2"}, false}, 31 - {"mkdir", "mkdir", nil, false}, 19 + {"", nil, false}, 20 + {"$", nil, true}, 21 + {"$", []string{"go", "run", "."}, true}, 22 + {"$$", nil, true}, 23 + {"$$", []string{"sleep", "10"}, true}, 24 + {"kill", []string{"1234"}, true}, 25 + {"kill", []string{"abc"}, true}, 26 + {"kill", nil, false}, 27 + {"kill", []string{"1", "2"}, false}, 28 + {"mkdir", []string{"dir"}, true}, 29 + {"mkdir", []string{"dir1", "dir2"}, true}, 30 + {"mkdir", nil, false}, 32 31 } 33 32 for _, tt := range tests { 34 - got := (&Tool{}).Match(&viye.Context{ 35 - Path: []string{tt.path}, 36 - Cmd: tt.cmd, 37 - Args: tt.args, 38 - }) 33 + got := (&Tool{}).Match(&viye.Context{Cmd: tt.cmd, Args: tt.args}) 39 34 is.Equal(t, tt.want, got) 40 35 } 41 36 }
M
internal/plumbing/url/url.go
··· 10 10 type Tool struct{} 11 11 12 12 func (Tool) Name() string { return "url(http://.*, https://.*)" } 13 -func (Tool) Match(ctx *viye.Context) bool { 14 - return strings.HasPrefix(ctx.Path[0], "http://") || strings.HasPrefix(ctx.Path[0], "https://") 13 +func (Tool) Match(c *viye.Context) bool { 14 + return strings.HasPrefix(c.Cmd, "http://") || strings.HasPrefix(c.Cmd, "https://") 15 15 } 16 16 17 -func (Tool) Execute(ctx *viye.Context) (string, error) { 18 - return "", osutil.Open(ctx.Path[0]) 17 +func (Tool) Execute(c *viye.Context) (string, error) { 18 + return "", osutil.Open(c.Cmd) 19 19 }
M
internal/viye/format.go
··· 18 18 return strings.Join(lines, "\n") + "\n" 19 19 } 20 20 21 -func FormatBulletList(s string) string { 22 - lines := strings.Split(s, "\n") 23 - for i, line := range lines { 21 +func FormatBulletListString(s string) string { 22 + return FormatBulletList(strings.Split(s, "\n")) 23 +} 24 + 25 +func FormatBulletList(s []string) string { 26 + for i, line := range s { 24 27 if line != "" { 25 - lines[i] = "- " + line 28 + s[i] = "- " + line 26 29 } 27 30 } 28 - return strings.Join(lines, "\n") 31 + return strings.Join(s, "\n") 29 32 }
M
internal/viye/viye.go
··· 6 6 "io" 7 7 "os" 8 8 "strings" 9 - 10 - "github.com/olexsmir/viye/internal/version" 11 9 ) 10 + 11 +const Version = "0.0.1-alpha" 12 12 13 13 var ErrPlumbingNotFound = errors.New("tool not found") 14 14 15 15 type Context struct { 16 - Path []string // ancestor chain [root, ..., target] 17 - Dir string // working directory 16 + Dir string // working directory 18 17 19 18 Cmd string // first word of the leaf path component 20 19 Args []string // remaining words of the leaf path component 20 + Path []string // ancestor chain [root, ..., target] 21 21 Body []string // body lines from : children (edited data for two-phase execution) 22 22 23 23 dispatch func(*Context) (string, error) // set by [Viye.dispatch] for tool chaining 24 24 } 25 25 26 26 // Next continues dispatch with currect [Context] state. 27 -// Call after consuming .Path[0] and updating .Dir. 28 27 func (c *Context) Next() (string, error) { return c.dispatch(c) } 29 28 30 29 type Tool interface { ··· 59 58 os.Exit(0) 60 59 } 61 60 62 - cmd, cmdArgs := splitLeaf(path[len(path)-1]) 63 61 res, err := v.dispatch(&Context{ 64 62 Path: path, 65 63 Dir: mustGetCwd(), 66 - Cmd: cmd, 67 - Args: cmdArgs, 68 64 Body: body, 69 65 }) 70 66 if err != nil { ··· 84 80 } 85 81 86 82 func (v *Viye) showVersion(out io.Writer) error { 87 - fmt.Fprint(out, "| viye version: "+version.Version) 83 + fmt.Fprint(out, "| viye version: "+Version) 88 84 return nil 89 85 } 90 86 ··· 93 89 if len(ctx.Path) == 0 { 94 90 return "", nil 95 91 } 92 + ctx.Cmd, ctx.Args = splitLeaf(ctx.Path[len(ctx.Path)-1]) 96 93 ctx.dispatch = v.dispatch 97 94 for _, tool := range v.tools { 98 95 if tool.Match(ctx) { ··· 103 100 } 104 101 105 102 // splitLeaf splits a leaf path component into command name and args. 106 -// For "mkdir dir" returns ("mkdir", []string{"dir"}). 107 103 // For "$ go run ." returns ("$", []string{"go", "run", "."}). 108 104 // For "ip" returns ("ip", nil). 109 105 func splitLeaf(leaf string) (cmd string, args []string) {
M
internal/viye/viye_test.go
··· 6 6 "olexsmir.xyz/x/is" 7 7 ) 8 8 9 -func TestSplitLeaf(t *testing.T) { 10 - tests := []struct { 11 - leaf, cmd string 12 - args []string 13 - }{ 14 - {"", "", nil}, 15 - {"ip", "ip", nil}, 16 - {"$", "$", nil}, 17 - {"$$", "$$", nil}, 18 - {"mkdir", "mkdir", nil}, 19 - {"$ go run .", "$", []string{"go", "run", "."}}, 20 - {"$$ sleep 10", "$$", []string{"sleep", "10"}}, 21 - {"mkdir dir", "mkdir", []string{"dir"}}, 22 - {"mkdir dir1 dir2", "mkdir", []string{"dir1", "dir2"}}, 23 - {"kill 1234", "kill", []string{"1234"}}, 24 - {" spaced ", "spaced", nil}, 25 - {"mkdir spaced dir", "mkdir", []string{"spaced", "dir"}}, 26 - {" ", "", nil}, 27 - } 28 - for _, tt := range tests { 29 - cmd, args := splitLeaf(tt.leaf) 30 - is.Equal(t, tt.cmd, cmd) 31 - is.Equal(t, len(tt.args), len(args)) 32 - if len(args) == len(tt.args) { 33 - for i := range args { 34 - is.Equal(t, tt.args[i], args[i]) 35 - } 36 - } 37 - } 38 -} 39 - 40 9 func TestSplitArgs(t *testing.T) { 41 10 tests := []struct{ args, want, wantB []string }{ 42 11 {[]string{"/tmp/foo", "bar/baz"}, []string{"/tmp/foo", "bar/baz"}, nil}, ··· 64 33 } 65 34 } 66 35 } 36 + 37 +func TestSplitLeaf(t *testing.T) { 38 + for _, tt := range []struct { 39 + leaf, cmd string 40 + want []string 41 + }{ 42 + {"mkdir dir", "mkdir", []string{"dir"}}, 43 + {"$ go run .", "$", []string{"go", "run", "."}}, 44 + {"ip", "ip", []string{}}, 45 + } { 46 + gotCmd, gotArgs := splitLeaf(tt.leaf) 47 + is.Equal(t, tt.cmd, gotCmd) 48 + is.Equal(t, tt.want, gotArgs) 49 + } 50 +}
M
plugin/viye.lua
··· 1 1 local function indent(t) return #t:match("^%s*") end 2 -local function is_data(line) return line:find("^%s*[|:]") ~= nil end 3 -local function is_node(line) return line:find("%S") and not is_data(line) end 4 - 5 -local function node_text(lnum) 6 - local l = vim.fn.getline(lnum) 7 - return l:match("^%s*[+-] (.*)") or vim.trim(l) 2 +local function line_type(line) 3 + if not line:find("%S") then return "empty" end 4 + return line:find("^%s*[|:]") and "data" or "node" 8 5 end 9 6 10 7 local function children_of(cmd_lnum) ··· 22 19 local d = indent(vim.fn.getline(lnum)) 23 20 for i = lnum - 1, 1, -1 do 24 21 local l = vim.fn.getline(i) 25 - if is_node(l) and indent(l) < d then return i end 22 + if line_type(l) == "node" and indent(l) < d then return i end 26 23 end 27 24 return nil 28 25 end ··· 31 28 local parts = {} 32 29 local lnum = cmd_lnum 33 30 while lnum do 34 - table.insert(parts, 1, node_text(lnum)) 31 + local l = vim.fn.getline(lnum) 32 + table.insert(parts, 1, l:match("^%s*[+-] (.*)") or vim.trim(l)) 35 33 lnum = parent_of(lnum) 36 34 end 37 35 return parts ··· 52 50 end 53 51 54 52 local function run(cmd_lnum) 55 - local parts = ancestors(cmd_lnum) 56 - if #parts == 0 then return nil end 57 - local args = vim.tbl_map(vim.fn.shellescape, parts) 53 + local cmd = ancestors(cmd_lnum) 54 + if #cmd == 0 then return end 55 + table.insert(cmd, 1, "viye") 56 + 58 57 local body = body_of(cmd_lnum) 59 - if #body > 0 then 60 - table.insert(args, "--") 61 - vim.list_extend(args, vim.tbl_map(vim.fn.shellescape, body)) 62 - end 63 - local cmd = "viye " .. table.concat(args, " ") 64 - vim.notify("viye: " .. cmd, vim.log.levels.DEBUG) 65 - local out = vim.fn.system(cmd) 66 - if vim.v.shell_error ~= 0 then 67 - vim.notify("viye: " .. vim.trim(out or ""), vim.log.levels.ERROR) 68 - return nil 69 - end 70 - return out 71 -end 58 + if #body > 0 then vim.list_extend(vim.list_extend(cmd, { "--" }), body) end 72 59 73 -local function delete_range(first, last) 74 - if last then vim.cmd(string.format("%d,%ddelete _", first, last)) end 60 + local res = vim.system(cmd, { text = true }):wait(2000) 61 + if res.code ~= 0 then 62 + vim.notify(vim.trim(res.stderr), vim.log.levels.ERROR) 63 + return 64 + end 65 + return res.stdout 75 66 end 76 67 77 68 local function insert_output(cmd_lnum, out) ··· 83 74 vim.fn.append(cmd_lnum, lines) 84 75 end 85 76 86 -local function toggle_bullet(cmd_lnum, to) 87 - vim.fn.setline(cmd_lnum, (vim.fn.getline(cmd_lnum):gsub("^[+-] ", to .. " ", 1))) 77 +local function toggle_bullet(cmd_lnum) 78 + vim.fn.setline(cmd_lnum, (vim.fn.getline(cmd_lnum):gsub("^[+-] ", function(c) 79 + return c == "+" and "-" or "+" 80 + end, 1))) 88 81 end 89 82 90 83 local function exec() 91 84 local lnum = vim.fn.line(".") 92 85 local line = vim.fn.getline(lnum) 93 - local cmd_lnum = is_data(line) and parent_of(lnum) or lnum 86 + local cmd_lnum = line_type(line) == "data" and parent_of(lnum) or lnum 94 87 if not cmd_lnum then return end 95 88 local first, last = children_of(cmd_lnum) 89 + local has_children = last ~= nil 96 90 local on_cmd = cmd_lnum == lnum 97 - local has_bullet = line:find("^%s*[+-] ") ~= nil 98 - local collapsing = last and ((on_cmd and has_bullet) or line:find("^%s*|") ~= nil) 99 - if collapsing then 100 - delete_range(first, last) 101 - toggle_bullet(cmd_lnum, "+") 91 + if has_children and (on_cmd or line_type(line) == "data") then 92 + if last then vim.cmd(string.format("%d,%ddelete _", first, last)) end 93 + toggle_bullet(cmd_lnum) 102 94 return 103 95 end 104 96 local out = run(cmd_lnum) 105 97 if not out then return end 106 - delete_range(first, last) 107 - if out ~= "" then 108 - insert_output(cmd_lnum, out) 109 - end 110 - toggle_bullet(cmd_lnum, "-") 98 + if last then vim.cmd(string.format("%d,%ddelete _", first, last)) end 99 + if out ~= "" then insert_output(cmd_lnum, out) end 100 + toggle_bullet(cmd_lnum) 111 101 end 112 102 113 103 vim.filetype.add { extension = { viye = "viye" } } 114 104 local group = vim.api.nvim_create_augroup("viye", { clear = true }) 115 - 116 105 vim.api.nvim_create_autocmd("BufEnter", { 117 106 pattern = "*", 118 107 group = group, ··· 120 109 if vim.api.nvim_buf_get_name(0) == "" then 121 110 vim.bo.filetype = "viye" 122 111 end 123 - end 112 + end, 124 113 }) 125 - 126 114 vim.api.nvim_create_autocmd("FileType", { 127 115 pattern = "viye", 128 116 group = group, 129 117 callback = function() 130 118 vim.keymap.set("n", "<CR>", exec, { buffer = true, silent = true }) 131 - end 119 + end, 132 120 })