all repos

viye @ d364a1ebe8c4550b026304ebafeb56b60eeb8ed3

my shot at reimplementing xiki
13 files changed, 113 insertions(+), 176 deletions(-)
refactor
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
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
         }

      
D internal/version/version.go
···
        1
        
        -package version

      
        2
        
        -

      
        3
        
        -const Version = "0.0.1-alpha"

      
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
         })