all repos

viye @ 0da3343

my shot at reimplementing xiki
13 files changed, 317 insertions(+), 53 deletions(-)
files: support ./ paths; add gobin; add json
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-23 12:27:11 +0300
Authored at: 2026-07-22 12:25:23 +0300
Change ID: zqvxoxkxpnzqylskylqsommxpmkqqrzz
Parent: 3fb8292
A .gitignore
···
        
        1
        +/viye

      
        
        2
        +/scratch.viye

      
M internal/plumbing/files/files.go
···
        11
        11
         

      
        12
        12
         type Tool struct{}

      
        13
        13
         

      
        14
        
        -// TODO: add ./ paths support

      
        15
        
        -

      
        16
        14
         func (Tool) Name() string { return "files(~/..., /...)" }

      
        17
        15
         

      
        18
        16
         func (Tool) Match(ctx *viye.Context) bool {

      
M internal/plumbing/files/files_test.go
···
        25
        25
         		{"f", dir, true},

      
        26
        26
         		{"nope", dir, false},

      
        27
        27
         		{"$ ls", dir, false},

      
        
        28
        +		{".", dir, true},

      
        
        29
        +		{"..", dir, true},

      
        28
        30
         	}

      
        29
        31
         	for _, tt := range tests {

      
        30
        32
         		got := (&Tool{}).Match(&viye.Context{Path: []string{tt.p}, Dir: tt.dir})

      ···
        83
        85
         		{"~/foo", ".", filepath.Join(home, "foo")},

      
        84
        86
         		{"/abs/path", ".", "/abs/path"},

      
        85
        87
         		{"rel", "/base", "/base/rel"},

      
        
        88
        +		{".", "/base", "/base"},

      
        
        89
        +		{"..", "/base/sub", "/base"},

      
        86
        90
         	}

      
        87
        91
         	for _, tt := range tests {

      
        88
        92
         		got := resolve(tt.p, tt.dir)

      
M internal/plumbing/gobin/gobin.go
···
        1
        1
         package gobin

      
        2
        2
         

      
        3
        
        -import "github.com/olexsmir/viye/internal/viye"

      
        
        3
        +import (

      
        
        4
        +	"os/exec"

      
        
        5
        +	"strings"

      
        
        6
        +

      
        
        7
        +	"github.com/olexsmir/viye/internal/viye"

      
        
        8
        +)

      
        4
        9
         

      
        5
        
        -const menu = `

      
        6
        
        -- build

      
        
        10
        +const menu = `- build

      
        7
        11
         - test

      
        
        12
        +- run

      
        8
        13
         - generate

      
        9
        14
         - doc

      
        10
        
        -- intall

      
        11
        
        -- mod

      
        
        15
        +- mod/

      
        12
        16
         `

      
        13
        17
         

      
        
        18
        +const modMenu = `- tidy

      
        
        19
        +- download

      
        
        20
        +- verify

      
        
        21
        +- init`

      
        
        22
        +

      
        14
        23
         type Tool struct{}

      
        15
        24
         

      
        16
        25
         func (Tool) Name() string { return "go" }

      
        17
        26
         func (Tool) Match(ctx *viye.Context) bool {

      
        18
        
        -	return ctx.Path[0] == "go"

      
        
        27
        +	return ctx.Cmd == "go" || (len(ctx.Path) > 0 && ctx.Path[0] == "go")

      
        19
        28
         }

      
        20
        29
         

      
        21
        30
         func (Tool) Execute(ctx *viye.Context) (string, error) {

      
        22
        
        -	switch ctx.Path[1] {

      
        23
        
        -	case "build":

      
        24
        
        -		return "| will build", nil

      
        25
        
        -	case "test":

      
        26
        
        -		return "| will test", nil

      
        27
        
        -	case "generate":

      
        28
        
        -		return "| will generate", nil

      
        29
        
        -	case "doc":

      
        30
        
        -		return "| will doc", nil

      
        31
        
        -	case "install":

      
        32
        
        -		return "| will install", nil

      
        
        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:]

      
        
        52
        +	}

      
        
        53
        +

      
        
        54
        +	switch subcmd {

      
        33
        55
         	case "mod":

      
        34
        
        -		return "| will mod", nil

      
        
        56
        +		if len(args) == 0 {

      
        
        57
        +			return modMenu, nil

      
        
        58
        +		}

      
        
        59
        +	case "build", "test", "generate", "doc", "install", "run", "fmt", "vet", "clean", "env", "fix", "list", "tool", "version":

      
        35
        60
         	default:

      
        36
        61
         		return menu, nil

      
        37
        62
         	}

      
        
        63
        +

      
        
        64
        +	return runGo(ctx.Dir, subcmd, args...)

      
        
        65
        +}

      
        
        66
        +

      
        
        67
        +func runGo(dir, subcmd string, args ...string) (string, error) {

      
        
        68
        +	cmd := exec.Command("go", append([]string{subcmd}, args...)...)

      
        
        69
        +	cmd.Dir = dir

      
        
        70
        +	out, _ := cmd.CombinedOutput()

      
        
        71
        +	return viye.Indent(string(out)), nil

      
        38
        72
         }

      
M internal/plumbing/gobin/gobin_test.go
···
        1
        1
         package gobin

      
        2
        2
         

      
        3
        
        -import "testing"

      
        
        3
        +import (

      
        
        4
        +	"testing"

      
        
        5
        +

      
        
        6
        +	"github.com/olexsmir/viye/internal/viye"

      
        
        7
        +)

      
        4
        8
         

      
        5
        9
         func TestMatch(t *testing.T) {

      
        
        10
        +	tests := []struct {

      
        
        11
        +		path []string

      
        
        12
        +		want bool

      
        
        13
        +	}{

      
        
        14
        +		{[]string{"go"}, true},

      
        
        15
        +		{[]string{"go", "build"}, true},

      
        
        16
        +		{[]string{"go", "test", "./..."}, true},

      
        
        17
        +		{[]string{"other"}, false},

      
        
        18
        +		{[]string{}, false},

      
        
        19
        +	}

      
        
        20
        +	for _, tt := range tests {

      
        
        21
        +		got := (Tool{}).Match(&viye.Context{Path: tt.path})

      
        
        22
        +		if got != tt.want {

      
        
        23
        +			t.Errorf("Match(%v) = %v; want %v", tt.path, got, tt.want)

      
        
        24
        +		}

      
        
        25
        +	}

      
        6
        26
         }

      
        7
        27
         

      
        8
        28
         func TestExecute(t *testing.T) {

      
        
        29
        +	t.Skipf("TODO implement")

      
        9
        30
         }

      
M internal/plumbing/ip/ip.go
···
        1
        1
         package ip

      
        2
        2
         

      
        3
        3
         import (

      
        
        4
        +	"context"

      
        4
        5
         	"errors"

      
        5
        6
         	"io"

      
        6
        7
         	"net"

      
        7
        8
         	"net/http"

      
        8
        9
         	"strings"

      
        
        10
        +	"time"

      
        9
        11
         

      
        10
        12
         	"github.com/olexsmir/viye/internal/viye"

      
        11
        13
         )

      ···
        53
        55
         }

      
        54
        56
         

      
        55
        57
         func getPublicIP() (string, error) {

      
        56
        
        -	resp, err := http.Get("https://ifconfig.me/ip")

      
        
        58
        +	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

      
        
        59
        +	defer cancel()

      
        
        60
        +

      
        
        61
        +	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://ifconfig.me/ip", nil)

      
        
        62
        +	if err != nil {

      
        
        63
        +		return "", err

      
        
        64
        +	}

      
        
        65
        +

      
        
        66
        +	resp, err := http.DefaultClient.Do(req)

      
        57
        67
         	if err != nil {

      
        58
        68
         		return "", err

      
        59
        69
         	}

      
A internal/plumbing/json/json.go
···
        
        1
        +package json

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"encoding/json"

      
        
        5
        +	"fmt"

      
        
        6
        +	"strings"

      
        
        7
        +

      
        
        8
        +	"github.com/olexsmir/viye/internal/viye"

      
        
        9
        +)

      
        
        10
        +

      
        
        11
        +type Tool struct{}

      
        
        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
        +	// Phase 1: no body → return editable template

      
        
        17
        +	if len(ctx.Body) == 0 {

      
        
        18
        +		return ": name: your name\n: age: 0\n", nil

      
        
        19
        +	}

      
        
        20
        +

      
        
        21
        +	// Phase 2: body present → parse and produce JSON

      
        
        22
        +	m := make(map[string]string)

      
        
        23
        +	for _, line := range ctx.Body {

      
        
        24
        +		// Strip ": " prefix

      
        
        25
        +		line = strings.TrimPrefix(line, ": ")

      
        
        26
        +		col := strings.Index(line, ":")

      
        
        27
        +		if col < 0 {

      
        
        28
        +			continue

      
        
        29
        +		}

      
        
        30
        +		key := strings.TrimSpace(line[:col])

      
        
        31
        +		val := strings.TrimSpace(line[col+1:])

      
        
        32
        +		if key != "" {

      
        
        33
        +			m[key] = val

      
        
        34
        +		}

      
        
        35
        +	}

      
        
        36
        +

      
        
        37
        +	b, err := json.Marshal(m)

      
        
        38
        +	if err != nil {

      
        
        39
        +		return "", fmt.Errorf("demo: %w", err)

      
        
        40
        +	}

      
        
        41
        +	return fmt.Sprintf("| %s\n", string(b)), nil

      
        
        42
        +}

      
M internal/plumbing/shell/shell.go
···
        25
        25
         		cmd := exec.Command("sh", "-c", strings.Join(ctx.Args, " "))

      
        26
        26
         		cmd.Dir = ctx.Dir

      
        27
        27
         		out, _ := cmd.CombinedOutput()

      
        28
        
        -

      
        29
        
        -		lines := strings.Split(string(out), "\n")

      
        30
        
        -		for i, line := range lines {

      
        31
        
        -			if line != "" {

      
        32
        
        -				lines[i] = "| " + line

      
        33
        
        -			}

      
        34
        
        -		}

      
        35
        
        -		return strings.Join(lines, "\n"), nil

      
        
        28
        +		return viye.Indent(string(out)), nil

      
        36
        29
         

      
        37
        30
         	case isBGCmd(ctx):

      
        38
        31
         		cmd := exec.Command("sh", "-c", strings.Join(ctx.Args, " "))

      
A internal/viye/format.go
···
        
        1
        +package viye

      
        
        2
        +

      
        
        3
        +import "strings"

      
        
        4
        +

      
        
        5
        +func Indent(s string) string {

      
        
        6
        +	s = strings.TrimSuffix(s, "\n")

      
        
        7
        +	if s == "" {

      
        
        8
        +		return ""

      
        
        9
        +	}

      
        
        10
        +	lines := strings.Split(s, "\n")

      
        
        11
        +	for i, line := range lines {

      
        
        12
        +		if line == "" {

      
        
        13
        +			lines[i] = "|"

      
        
        14
        +		} else {

      
        
        15
        +			lines[i] = "| " + line

      
        
        16
        +		}

      
        
        17
        +	}

      
        
        18
        +	return strings.Join(lines, "\n") + "\n"

      
        
        19
        +}

      
M internal/viye/viye.go
···
        13
        13
         

      
        14
        14
         	Cmd  string   // first word of the leaf path component

      
        15
        15
         	Args []string // remaining words of the leaf path component

      
        
        16
        +	Body []string // body lines from : children (edited data for two-phase execution)

      
        16
        17
         

      
        17
        18
         	dispatch func(*Context) (string, error) // set by [Viye.dispatch] for tool chaining

      
        18
        19
         }

      ···
        45
        46
         		return v.showHelp(out)

      
        46
        47
         	}

      
        47
        48
         

      
        48
        
        -	path := splitArgs(args[1:])

      
        
        49
        +	path, body := splitArgs(args[1:])

      
        49
        50
         	if len(path) == 0 {

      
        50
        51
         		os.Exit(0)

      
        51
        52
         	}

      ···
        56
        57
         		Dir:  mustGetCwd(),

      
        57
        58
         		Cmd:  cmd,

      
        58
        59
         		Args: cmdArgs,

      
        
        60
        +		Body: body,

      
        59
        61
         	})

      
        60
        62
         	if err != nil {

      
        61
        63
         		return err

      ···
        99
        101
         	return parts[0], parts[1:]

      
        100
        102
         }

      
        101
        103
         

      
        102
        
        -// splitArgs builds path by splitting each arg on "/"

      
        
        104
        +// splitArgs builds path by splitting each arg on "/" and body on "--".

      
        
        105
        +// Everything before "--" is the path (split into components).

      
        
        106
        +// Everything after "--" is the body (kept as-is, one element per line).

      
        
        107
        +// If there's no "--", body is nil.

      
        103
        108
         // for absolute paths(/...), leading / is preserved as part of first component.

      
        104
        109
         // for home paths(~...), ~ is the first comonents

      
        105
        110
         // urls are kept as single component

      
        106
        
        -func splitArgs(args []string) []string {

      
        107
        
        -	var path []string

      
        
        111
        +func splitArgs(args []string) (path []string, body []string) {

      
        
        112
        +	sawSep := false

      
        108
        113
         	for _, arg := range args {

      
        109
        
        -		path = append(path, splitArg(arg)...)

      
        
        114
        +		if !sawSep && arg == "--" {

      
        
        115
        +			sawSep = true

      
        
        116
        +			continue

      
        
        117
        +		}

      
        
        118
        +		if !sawSep {

      
        
        119
        +			path = append(path, splitArg(arg)...)

      
        
        120
        +		} else {

      
        
        121
        +			body = append(body, arg)

      
        
        122
        +		}

      
        110
        123
         	}

      
        111
        
        -	return path

      
        
        124
        +	return

      
        112
        125
         }

      
        113
        126
         

      
        114
        127
         func splitArg(arg string) []string {

      ···
        144
        157
         	case strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://"):

      
        145
        158
         		return []string{arg}

      
        146
        159
         	default:

      
        147
        
        -		parts := strings.Split(arg, "/")

      
        
        160
        +		s := strings.TrimPrefix(arg, "./")

      
        
        161
        +		if s != arg {

      
        
        162
        +			if s == "" {

      
        
        163
        +				return []string{"."}

      
        
        164
        +			}

      
        
        165
        +			// No "/" left to split on — keep original (e.g. "./..." stays "./...")

      
        
        166
        +			if !strings.Contains(s, "/") {

      
        
        167
        +				return []string{arg}

      
        
        168
        +			}

      
        
        169
        +			return splitArg(s)

      
        
        170
        +		}

      
        
        171
        +		parts := strings.Split(s, "/")

      
        148
        172
         		var result []string

      
        149
        173
         		for _, p := range parts {

      
        150
        174
         			if p != "" {

      
M internal/viye/viye_test.go
···
        62
        62
         		{"~/foo", []string{"~", "foo"}},

      
        63
        63
         		{"~/a/b", []string{"~", "a", "b"}},

      
        64
        64
         

      
        
        65
        +		// ./ paths

      
        
        66
        +		{"./", []string{"."}},

      
        
        67
        +		{"./foo", []string{"./foo"}},

      
        
        68
        +		{"./foo/bar", []string{"foo", "bar"}},

      
        
        69
        +		{"././foo", []string{"./foo"}},

      
        
        70
        +		{"./././", []string{"."}},

      
        
        71
        +		{".", []string{"."}},

      
        
        72
        +		{"..", []string{".."}},

      
        
        73
        +		{"../foo", []string{"..", "foo"}},

      
        
        74
        +		{"./../foo", []string{"..", "foo"}},

      
        
        75
        +

      
        65
        76
         		// urls

      
        66
        77
         		{"http://example.com", []string{"http://example.com"}},

      
        67
        78
         		{"https://example.com/path", []string{"https://example.com/path"}},

      ···
        82
        93
         }

      
        83
        94
         

      
        84
        95
         func TestSplitArgs(t *testing.T) {

      
        85
        
        -	got := splitArgs([]string{"/tmp/foo", "bar/baz"})

      
        86
        
        -	want := []string{"/tmp", "foo", "bar", "baz"}

      
        87
        
        -	if len(got) != len(want) {

      
        88
        
        -		t.Errorf("splitArgs = %v; want %v", got, want)

      
        89
        
        -	} else {

      
        
        96
        +	tests := []struct {

      
        
        97
        +		args    []string

      
        
        98
        +		want    []string

      
        
        99
        +		wantB   []string

      
        
        100
        +	}{

      
        
        101
        +		{[]string{"/tmp/foo", "bar/baz"}, []string{"/tmp", "foo", "bar", "baz"}, nil},

      
        
        102
        +		{[]string{"./foo", "bar"}, []string{"./foo", "bar"}, nil},

      
        
        103
        +		{[]string{"./"}, []string{"."}, nil},

      
        
        104
        +		{[]string{"././foo", "./bar"}, []string{"./foo", "./bar"}, nil},

      
        
        105
        +		{[]string{"demo"}, []string{"demo"}, nil},

      
        
        106
        +		{[]string{"demo", "--", ": name: olex"}, []string{"demo"}, []string{": name: olex"}},

      
        
        107
        +		{[]string{"demo", "--", ": name: olex", ": age: 30"}, []string{"demo"}, []string{": name: olex", ": age: 30"}},

      
        
        108
        +		{[]string{"--", ": body"}, nil, []string{": body"}},

      
        
        109
        +	}

      
        
        110
        +	for _, tt := range tests {

      
        
        111
        +		got, gotB := splitArgs(tt.args)

      
        
        112
        +		if len(got) != len(tt.want) {

      
        
        113
        +			t.Errorf("splitArgs(%v) path = %v; want %v", tt.args, got, tt.want)

      
        
        114
        +			continue

      
        
        115
        +		}

      
        90
        116
         		for i := range got {

      
        91
        
        -			if got[i] != want[i] {

      
        92
        
        -				t.Errorf("splitArgs = %v; want %v", got, want)

      
        
        117
        +			if got[i] != tt.want[i] {

      
        
        118
        +				t.Errorf("splitArgs(%v) path = %v; want %v", tt.args, got, tt.want)

      
        
        119
        +				break

      
        
        120
        +			}

      
        
        121
        +		}

      
        
        122
        +		if len(gotB) != len(tt.wantB) {

      
        
        123
        +			t.Errorf("splitArgs(%v) body = %v; want %v", tt.args, gotB, tt.wantB)

      
        
        124
        +			continue

      
        
        125
        +		}

      
        
        126
        +		for i := range gotB {

      
        
        127
        +			if gotB[i] != tt.wantB[i] {

      
        
        128
        +				t.Errorf("splitArgs(%v) body = %v; want %v", tt.args, gotB, tt.wantB)

      
        93
        129
         				break

      
        94
        130
         			}

      
        95
        131
         		}

      
M lua/viye.lua
···
        39
        39
           return next_line ~= "" and indent(next_line) > depth

      
        40
        40
         end

      
        41
        41
         

      
        
        42
        +-- Collect : body lines under the current line (siblings at depth+2)

      
        
        43
        +-- Returns list of stripped lines (with ": " prefix preserved)

      
        
        44
        +local function collect_body()

      
        
        45
        +  local lnum = vim.fn.line "."

      
        
        46
        +  local depth = indent(vim.fn.getline(lnum))

      
        
        47
        +  local body = {}

      
        
        48
        +  local i = lnum + 1

      
        
        49
        +  while i <= vim.fn.line "$" do

      
        
        50
        +    local l = vim.fn.getline(i)

      
        
        51
        +    if l == "" then break end

      
        
        52
        +    local d = indent(l)

      
        
        53
        +    if d <= depth then break end

      
        
        54
        +    if d == depth + 2 and l:find("^%s*: ") then

      
        
        55
        +      table.insert(body, vim.trim(l))

      
        
        56
        +    end

      
        
        57
        +    i = i + 1

      
        
        58
        +  end

      
        
        59
        +  return body

      
        
        60
        +end

      
        
        61
        +

      
        
        62
        +-- Remove | output lines under the current line (siblings at depth+2)

      
        
        63
        +-- Returns the line number after the removed block, or nil if nothing removed

      
        
        64
        +local function remove_output()

      
        
        65
        +  local lnum = vim.fn.line "."

      
        
        66
        +  local depth = indent(vim.fn.getline(lnum))

      
        
        67
        +  local first = nil

      
        
        68
        +  local last = nil

      
        
        69
        +  local i = lnum + 1

      
        
        70
        +  while i <= vim.fn.line "$" do

      
        
        71
        +    local l = vim.fn.getline(i)

      
        
        72
        +    if l == "" then break end

      
        
        73
        +    local d = indent(l)

      
        
        74
        +    if d <= depth then break end

      
        
        75
        +    if d == depth + 2 and l:find("^%s*| ") then

      
        
        76
        +      if first == nil then first = i end

      
        
        77
        +      last = i

      
        
        78
        +    end

      
        
        79
        +    i = i + 1

      
        
        80
        +  end

      
        
        81
        +  if first then

      
        
        82
        +    vim.cmd(string.format("%d,%ddelete _", first, last))

      
        
        83
        +    return first

      
        
        84
        +  end

      
        
        85
        +  return nil

      
        
        86
        +end

      
        
        87
        +

      
        42
        88
         local function collapse()

      
        43
        89
           local lnum = vim.fn.line "."

      
        44
        90
           local depth = indent(vim.fn.getline(lnum))

      ···
        64
        110
           if toggled ~= line then vim.fn.setline(lnum, toggled) end

      
        65
        111
         end

      
        66
        112
         

      
        67
        
        -local function insert_output(output)

      
        68
        
        -  local lnum = vim.fn.line "."

      
        69
        
        -  local pad = string.rep(" ", indent(vim.fn.getline(lnum)) + 2)

      
        
        113
        +local function insert_output(output, after_lnum)

      
        
        114
        +  after_lnum = after_lnum or vim.fn.line "."

      
        
        115
        +  local pad = string.rep(" ", indent(vim.fn.getline(after_lnum)) + 2)

      
        70
        116
           local lines = vim.split(output, "\n", { trimempty = true })

      
        71
        117
           for i, line in ipairs(lines) do lines[i] = pad .. line end

      
        72
        
        -  vim.fn.append(lnum, lines)

      
        
        118
        +  vim.fn.append(after_lnum, lines)

      
        
        119
        +end

      
        
        120
        +

      
        
        121
        +-- Find the last child line at depth+2 under the current line

      
        
        122
        +local function find_last_child()

      
        
        123
        +  local lnum = vim.fn.line "."

      
        
        124
        +  local depth = indent(vim.fn.getline(lnum))

      
        
        125
        +  local last = lnum

      
        
        126
        +  local i = lnum + 1

      
        
        127
        +  while i <= vim.fn.line "$" do

      
        
        128
        +    local l = vim.fn.getline(i)

      
        
        129
        +    if l == "" then break end

      
        
        130
        +    local d = indent(l)

      
        
        131
        +    if d <= depth then break end

      
        
        132
        +    if d == depth + 2 then

      
        
        133
        +      last = i

      
        
        134
        +    end

      
        
        135
        +    i = i + 1

      
        
        136
        +  end

      
        
        137
        +  return last

      
        73
        138
         end

      
        74
        139
         

      
        75
        
        -local function exec()

      
        
        140
        +local function exec(reenter)

      
        76
        141
           local line = vim.fn.getline "."

      
        77
        142
           if is_data(line) then

      
        78
        143
             local lnum = vim.fn.line "."

      ···
        82
        147
               local l = vim.fn.getline(i)

      
        83
        148
               if not is_data(l) and indent(l) < depth and l:find "%S" then

      
        84
        149
                 vim.fn.cursor(i, 1)

      
        85
        
        -        exec()

      
        
        150
        +        exec(true)

      
        86
        151
                 return

      
        87
        152
               end

      
        88
        153
               i = i - 1

      ···
        90
        155
             return

      
        91
        156
           end

      
        92
        157
         

      
        93
        
        -  if has_children() then

      
        
        158
        +  if not reenter and has_children() then

      
        94
        159
             collapse()

      
        95
        160
             toggle_bullet()

      
        96
        161
             return

      ···
        99
        164
           local path = build_path()

      
        100
        165
           if path == "" then return end

      
        101
        166
         

      
        
        167
        +  local body = reenter and collect_body() or {}

      
        
        168
        +

      
        102
        169
           local cmd = "viye " .. vim.fn.shellescape(path)

      
        
        170
        +  if #body > 0 then

      
        
        171
        +    cmd = cmd .. " --"

      
        
        172
        +    for _, line in ipairs(body) do

      
        
        173
        +      cmd = cmd .. " " .. vim.fn.shellescape(line)

      
        
        174
        +    end

      
        
        175
        +  end

      
        103
        176
           vim.notify("viye: " .. cmd, vim.log.levels.INFO)

      
        104
        177
         

      
        105
        178
           local out = vim.fn.system(cmd)

      ···
        109
        182
           end

      
        110
        183
         

      
        111
        184
           if out ~= "" then

      
        112
        
        -    insert_output(out)

      
        113
        
        -    toggle_bullet()

      
        
        185
        +    remove_output()

      
        
        186
        +    local target = reenter and find_last_child() or vim.fn.line "."

      
        
        187
        +    insert_output(out, target)

      
        
        188
        +    if not reenter then

      
        
        189
        +      toggle_bullet()

      
        
        190
        +    end

      
        114
        191
           end

      
        115
        192
         end

      
        116
        193
         

      
M main.go
···
        4
        4
         	"fmt"

      
        5
        5
         	"os"

      
        6
        6
         

      
        
        7
        +	"github.com/olexsmir/viye/internal/plumbing/json"

      
        7
        8
         	"github.com/olexsmir/viye/internal/plumbing/files"

      
        
        9
        +	"github.com/olexsmir/viye/internal/plumbing/gobin"

      
        8
        10
         	"github.com/olexsmir/viye/internal/plumbing/ip"

      
        9
        11
         	"github.com/olexsmir/viye/internal/plumbing/shell"

      
        10
        12
         	"github.com/olexsmir/viye/internal/plumbing/url"

      ···
        17
        19
         	v.Register(&shell.Tool{})

      
        18
        20
         	v.Register(&url.Tool{})

      
        19
        21
         	v.Register(&ip.Tool{})

      
        
        22
        +	v.Register(&gobin.Tool{})

      
        
        23
        +	v.Register(&json.Tool{})

      
        20
        24
         

      
        21
        25
         	if err := v.Run(os.Stdout, os.Args); err != nil {

      
        22
        26
         		fmt.Fprintf(os.Stderr, "viye: %v\n", err)