all repos

viye @ 63835f54e77ecc1ae8e8ca126fffab519544f58e

my shot at reimplementing xiki
11 files changed, 656 insertions(+), 0 deletions(-)
init
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-22 00:21:05 +0300
Authored at: 2026-07-21 18:11:47 +0300
Change ID: swmmotpymxtnpxlmkznsvlzrvstzpxqm
A core/core.go
···
        
        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
        +}

      
A core/osutil/osutil_linux.go
···
        
        1
        +//go:build linux

      
        
        2
        +

      
        
        3
        +package osutil

      
        
        4
        +

      
        
        5
        +import (

      
        
        6
        +	"os/exec"

      
        
        7
        +	"syscall"

      
        
        8
        +)

      
        
        9
        +

      
        
        10
        +func Open(path string) error {

      
        
        11
        +	cmd := exec.Command("xdg-open", path)

      
        
        12
        +	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        
        13
        +	cmd.Start()

      
        
        14
        +	return nil

      
        
        15
        +}

      
A go.mod
···
        
        1
        +module github.com/olexsmir/viye

      
        
        2
        +

      
        
        3
        +go 1.26.4

      
A lua/viye.lua
···
        
        1
        +local M = {}

      
        
        2
        +

      
        
        3
        +local function indent(text) return #text:match "^%s*" end

      
        
        4
        +local function is_data(text) return text:find "^%s*[|:] " ~= nil end

      
        
        5
        +local function strip_bullet(text) return (text:gsub("^[+-] ", "", 1)) end

      
        
        6
        +

      
        
        7
        +local function build_path()

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

      
        
        9
        +  local line = vim.fn.getline(lnum)

      
        
        10
        +  local depth = indent(line)

      
        
        11
        +  local parts = { strip_bullet(vim.trim(line)) }

      
        
        12
        +

      
        
        13
        +  local i = lnum - 1

      
        
        14
        +  while i >= 1 do

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

      
        
        16
        +    local d = indent(l)

      
        
        17
        +    if d == 0 and l:find "%S" and depth > 0 then

      
        
        18
        +      table.insert(parts, 1, strip_bullet(vim.trim(l)))

      
        
        19
        +      break

      
        
        20
        +    end

      
        
        21
        +    if d < depth and l:find "%S" then

      
        
        22
        +      table.insert(parts, 1, strip_bullet(vim.trim(l)))

      
        
        23
        +      depth = d

      
        
        24
        +    end

      
        
        25
        +    i = i - 1

      
        
        26
        +  end

      
        
        27
        +

      
        
        28
        +  for idx = 1, #parts - 1 do

      
        
        29
        +    parts[idx] = parts[idx]:gsub("/+$", "")

      
        
        30
        +  end

      
        
        31
        +

      
        
        32
        +  return table.concat(parts, "/")

      
        
        33
        +end

      
        
        34
        +

      
        
        35
        +local function has_children()

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

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

      
        
        38
        +  local next_line = vim.fn.getline(lnum + 1)

      
        
        39
        +  return next_line ~= "" and indent(next_line) > depth

      
        
        40
        +end

      
        
        41
        +

      
        
        42
        +local function collapse()

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

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

      
        
        45
        +  local s = lnum + 1

      
        
        46
        +  local e = s

      
        
        47
        +  while e <= vim.fn.line "$" do

      
        
        48
        +    local l = vim.fn.getline(e)

      
        
        49
        +    if l == "" or indent(l) <= depth then break end

      
        
        50
        +    e = e + 1

      
        
        51
        +  end

      
        
        52
        +  if e > s then vim.cmd(string.format("%d,%ddelete _", s, e - 1)) end

      
        
        53
        +end

      
        
        54
        +

      
        
        55
        +local function toggle_bullet()

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

      
        
        57
        +  local line = vim.fn.getline(lnum)

      
        
        58
        +  local toggled = line:gsub("^%+ ", "- ", 1)

      
        
        59
        +  if toggled ~= line then

      
        
        60
        +    vim.fn.setline(lnum, toggled)

      
        
        61
        +    return

      
        
        62
        +  end

      
        
        63
        +  toggled = line:gsub("^%- ", "+ ", 1)

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

      
        
        65
        +end

      
        
        66
        +

      
        
        67
        +local function insert_output(output)

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

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

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

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

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

      
        
        73
        +end

      
        
        74
        +

      
        
        75
        +local function exec()

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

      
        
        77
        +  if is_data(line) then

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

      
        
        79
        +    local depth = indent(line)

      
        
        80
        +    local i = lnum - 1

      
        
        81
        +    while i >= 1 do

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

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

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

      
        
        85
        +        exec()

      
        
        86
        +        return

      
        
        87
        +      end

      
        
        88
        +      i = i - 1

      
        
        89
        +    end

      
        
        90
        +    return

      
        
        91
        +  end

      
        
        92
        +

      
        
        93
        +  if has_children() then

      
        
        94
        +    collapse()

      
        
        95
        +    toggle_bullet()

      
        
        96
        +    return

      
        
        97
        +  end

      
        
        98
        +

      
        
        99
        +  local path = build_path()

      
        
        100
        +  if path == "" then return end

      
        
        101
        +

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

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

      
        
        104
        +

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

      
        
        106
        +  if vim.v.shell_error ~= 0 then

      
        
        107
        +    vim.notify("viye: " .. out:gsub("\n$", ""), vim.log.levels.ERROR)

      
        
        108
        +    return

      
        
        109
        +  end

      
        
        110
        +

      
        
        111
        +  if out ~= "" then

      
        
        112
        +    insert_output(out)

      
        
        113
        +    toggle_bullet()

      
        
        114
        +  end

      
        
        115
        +end

      
        
        116
        +

      
        
        117
        +function M.setup()

      
        
        118
        +  vim.filetype.add { extension = { viye = "viye" } }

      
        
        119
        +  vim.api.nvim_create_autocmd("FileType", {

      
        
        120
        +    pattern = "viye",

      
        
        121
        +    group = vim.api.nvim_create_augroup("viye_plugin", { clear = true }),

      
        
        122
        +    callback = function() vim.keymap.set("n", "<CR>", exec, { buffer = true, silent = true }) end,

      
        
        123
        +  })

      
        
        124
        +end

      
        
        125
        +

      
        
        126
        +return M

      
A main.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"os"

      
        
        6
        +

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

      
        
        8
        +	"github.com/olexsmir/viye/plumbing/files"

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

      
        
        10
        +	"github.com/olexsmir/viye/plumbing/ip"

      
        
        11
        +	"github.com/olexsmir/viye/plumbing/shell"

      
        
        12
        +	"github.com/olexsmir/viye/plumbing/url"

      
        
        13
        +)

      
        
        14
        +

      
        
        15
        +func main() {

      
        
        16
        +	v := core.New()

      
        
        17
        +	v.Register(&files.Tool{})

      
        
        18
        +	v.Register(&shell.Tool{})

      
        
        19
        +	v.Register(&url.Tool{})

      
        
        20
        +	v.Register(&ip.Tool{})

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

      
        
        22
        +

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

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

      
        
        25
        +		os.Exit(1)

      
        
        26
        +	}

      
        
        27
        +}

      
A plumbing/files/files.go
···
        
        1
        +package files

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"os"

      
        
        5
        +	"os/exec"

      
        
        6
        +	"path/filepath"

      
        
        7
        +	"strings"

      
        
        8
        +

      
        
        9
        +	"github.com/olexsmir/viye/core"

      
        
        10
        +	"github.com/olexsmir/viye/core/osutil"

      
        
        11
        +)

      
        
        12
        +

      
        
        13
        +type Tool struct{}

      
        
        14
        +

      
        
        15
        +func (Tool) Name() string { return "files(~/..., /...)" }

      
        
        16
        +func (Tool) Match(ctx *core.Context) bool {

      
        
        17
        +	p := ctx.Path[0]

      
        
        18
        +	if p == "~" || strings.HasPrefix(p, "/") {

      
        
        19
        +		return true

      
        
        20
        +	}

      
        
        21
        +	_, err := os.Stat(filepath.Join(ctx.Dir, p))

      
        
        22
        +	return err == nil

      
        
        23
        +}

      
        
        24
        +

      
        
        25
        +func (Tool) Execute(ctx *core.Context) (string, error) {

      
        
        26
        +	path := resolve(ctx.Path[0], ctx.Dir)

      
        
        27
        +

      
        
        28
        +	info, err := os.Stat(path)

      
        
        29
        +	if err != nil {

      
        
        30
        +		return "", err

      
        
        31
        +	}

      
        
        32
        +

      
        
        33
        +	switch {

      
        
        34
        +	case info.IsDir():

      
        
        35
        +		if len(ctx.Path) == 1 {

      
        
        36
        +			return listDir(path)

      
        
        37
        +		}

      
        
        38
        +		ctx.Dir = path

      
        
        39
        +		ctx.Path = ctx.Path[1:]

      
        
        40
        +		return ctx.Next()

      
        
        41
        +

      
        
        42
        +	case info.Mode()&0o111 != 0:

      
        
        43
        +		return runExec(path)

      
        
        44
        +

      
        
        45
        +	default:

      
        
        46
        +		return "", osutil.Open(path)

      
        
        47
        +	}

      
        
        48
        +}

      
        
        49
        +

      
        
        50
        +func resolve(p, dir string) string {

      
        
        51
        +	if p == "~" {

      
        
        52
        +		home, _ := os.UserHomeDir()

      
        
        53
        +		return home

      
        
        54
        +	}

      
        
        55
        +	if strings.HasPrefix(p, "~/") {

      
        
        56
        +		home, _ := os.UserHomeDir()

      
        
        57
        +		return filepath.Join(home, p[2:])

      
        
        58
        +	}

      
        
        59
        +	if filepath.IsAbs(p) {

      
        
        60
        +		return filepath.Clean(p)

      
        
        61
        +	}

      
        
        62
        +	return filepath.Join(dir, p)

      
        
        63
        +}

      
        
        64
        +

      
        
        65
        +func listDir(dir string) (string, error) {

      
        
        66
        +	entries, err := os.ReadDir(dir)

      
        
        67
        +	if err != nil {

      
        
        68
        +		return "", err

      
        
        69
        +	}

      
        
        70
        +	var b strings.Builder

      
        
        71
        +	for _, e := range entries {

      
        
        72
        +		name := e.Name()

      
        
        73
        +		if e.IsDir() {

      
        
        74
        +			name += "/"

      
        
        75
        +		}

      
        
        76
        +		b.WriteString(name)

      
        
        77
        +		b.WriteByte('\n')

      
        
        78
        +	}

      
        
        79
        +	return b.String(), nil

      
        
        80
        +}

      
        
        81
        +

      
        
        82
        +func runExec(path string) (string, error) {

      
        
        83
        +	cmd := exec.Command(path)

      
        
        84
        +	out, err := cmd.Output()

      
        
        85
        +	if err != nil {

      
        
        86
        +		return "", err

      
        
        87
        +	}

      
        
        88
        +	return string(out), nil

      
        
        89
        +}

      
A plumbing/files/files_test.go
···
        
        1
        +package files

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"os"

      
        
        5
        +	"path/filepath"

      
        
        6
        +	"strings"

      
        
        7
        +	"testing"

      
        
        8
        +

      
        
        9
        +	"github.com/olexsmir/viye/core"

      
        
        10
        +)

      
        
        11
        +

      
        
        12
        +func TestMatch(t *testing.T) {

      
        
        13
        +	dir := t.TempDir()

      
        
        14
        +	os.Mkdir(filepath.Join(dir, "sub"), 0o755)

      
        
        15
        +	os.WriteFile(filepath.Join(dir, "f"), nil, 0o644)

      
        
        16
        +

      
        
        17
        +	tests := []struct {

      
        
        18
        +		p, dir string

      
        
        19
        +		m      bool

      
        
        20
        +	}{

      
        
        21
        +		{"~", ".", true},

      
        
        22
        +		{"/tmp", ".", true},

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

      
        
        24
        +		{"sub", dir, true},

      
        
        25
        +		{"f", dir, true},

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

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

      
        
        28
        +	}

      
        
        29
        +	for _, tt := range tests {

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

      
        
        31
        +		if got != tt.m {

      
        
        32
        +			t.Errorf("Match(%q, dir=%q) = %v; want %v", tt.p, tt.dir, got, tt.m)

      
        
        33
        +		}

      
        
        34
        +	}

      
        
        35
        +}

      
        
        36
        +

      
        
        37
        +func TestExecute(t *testing.T) {

      
        
        38
        +	t.Run("list dir", func(t *testing.T) {

      
        
        39
        +		dir := t.TempDir()

      
        
        40
        +		os.WriteFile(filepath.Join(dir, "x"), nil, 0o644)

      
        
        41
        +		ctx := &core.Context{Path: []string{dir}, Dir: "."}

      
        
        42
        +		got, err := (&Tool{}).Execute(ctx)

      
        
        43
        +		if err != nil {

      
        
        44
        +			t.Fatal(err)

      
        
        45
        +		}

      
        
        46
        +		if got != "x\n" {

      
        
        47
        +			t.Errorf("Execute = %q; want %q", got, "x\\n")

      
        
        48
        +		}

      
        
        49
        +	})

      
        
        50
        +

      
        
        51
        +	t.Run("navigate into subdir", func(t *testing.T) {

      
        
        52
        +		dir := t.TempDir()

      
        
        53
        +		sub := filepath.Join(dir, "sub")

      
        
        54
        +		os.Mkdir(sub, 0o755)

      
        
        55
        +		os.WriteFile(filepath.Join(sub, "y"), nil, 0o644)

      
        
        56
        +

      
        
        57
        +		v := core.New()

      
        
        58
        +		v.Register(&Tool{})

      
        
        59
        +		var out strings.Builder

      
        
        60
        +		if err := v.Run(&out, []string{"viye", dir, "sub"}); err != nil {

      
        
        61
        +			t.Fatal(err)

      
        
        62
        +		}

      
        
        63
        +		if out.String() != "y\n" {

      
        
        64
        +			t.Errorf("Run = %q; want %q", out.String(), "y\\n")

      
        
        65
        +		}

      
        
        66
        +	})

      
        
        67
        +

      
        
        68
        +	t.Run("non existent path", func(t *testing.T) {

      
        
        69
        +		ctx := &core.Context{Path: []string{"/nonexistent_foobar"}, Dir: "."}

      
        
        70
        +		if _, err := (&Tool{}).Execute(ctx); err == nil {

      
        
        71
        +			t.Error("expected error")

      
        
        72
        +		}

      
        
        73
        +	})

      
        
        74
        +}

      
        
        75
        +

      
        
        76
        +func TestResolve(t *testing.T) {

      
        
        77
        +	home, _ := os.UserHomeDir()

      
        
        78
        +	tests := []struct {

      
        
        79
        +		p, dir string

      
        
        80
        +		want   string

      
        
        81
        +	}{

      
        
        82
        +		{"~", ".", home},

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

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

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

      
        
        86
        +	}

      
        
        87
        +	for _, tt := range tests {

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

      
        
        89
        +		if got != tt.want {

      
        
        90
        +			t.Errorf("resolve(%q, %q) = %q; want %q", tt.p, tt.dir, got, tt.want)

      
        
        91
        +		}

      
        
        92
        +	}

      
        
        93
        +}

      
        
        94
        +

      
        
        95
        +func TestListDir(t *testing.T) {

      
        
        96
        +	dir := t.TempDir()

      
        
        97
        +	os.Mkdir(filepath.Join(dir, "sub"), 0o755)

      
        
        98
        +	os.WriteFile(filepath.Join(dir, "a.txt"), nil, 0o644)

      
        
        99
        +

      
        
        100
        +	got, err := listDir(dir)

      
        
        101
        +	if err != nil {

      
        
        102
        +		t.Fatal(err)

      
        
        103
        +	}

      
        
        104
        +	if got != "a.txt\nsub/\n" {

      
        
        105
        +		t.Errorf("listDir = %q; want %q", got, "a.txt\\nsub/\\n")

      
        
        106
        +	}

      
        
        107
        +}

      
A plumbing/gobin/gobin.go
···
        
        1
        +package gobin

      
        
        2
        +

      
        
        3
        +import "github.com/olexsmir/viye/core"

      
        
        4
        +

      
        
        5
        +const menu = `

      
        
        6
        +- build

      
        
        7
        +- test

      
        
        8
        +- generate

      
        
        9
        +- doc

      
        
        10
        +- intall

      
        
        11
        +- mod

      
        
        12
        +`

      
        
        13
        +

      
        
        14
        +type Tool struct{}

      
        
        15
        +

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

      
        
        17
        +func (Tool) Match(ctx *core.Context) bool {

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

      
        
        19
        +}

      
        
        20
        +

      
        
        21
        +func (Tool) Execute(ctx *core.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

      
        
        33
        +	case "mod":

      
        
        34
        +		return "| will mod", nil

      
        
        35
        +	default:

      
        
        36
        +		return menu, nil

      
        
        37
        +	}

      
        
        38
        +}

      
A plumbing/ip/ip.go
···
        
        1
        +package ip

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"errors"

      
        
        5
        +	"io"

      
        
        6
        +	"net"

      
        
        7
        +	"net/http"

      
        
        8
        +	"strings"

      
        
        9
        +

      
        
        10
        +	"github.com/olexsmir/viye/core"

      
        
        11
        +)

      
        
        12
        +

      
        
        13
        +type Tool struct{}

      
        
        14
        +

      
        
        15
        +func (Tool) Name() string                 { return "ip" }

      
        
        16
        +func (Tool) Match(ctx *core.Context) bool { return len(ctx.Path) == 1 && ctx.Path[0] == "ip" }

      
        
        17
        +func (Tool) Execute(ctx *core.Context) (string, error) {

      
        
        18
        +	// TODO: if it's not possile to get  public ip, report only local

      
        
        19
        +

      
        
        20
        +	var buf strings.Builder

      
        
        21
        +	buf.WriteString("| ")

      
        
        22
        +

      
        
        23
        +	pip, err := getPublicIP()

      
        
        24
        +	if err != nil {

      
        
        25
        +		return "", err

      
        
        26
        +	}

      
        
        27
        +	_, _ = buf.WriteString(pip)

      
        
        28
        +	_, _ = buf.WriteString(" / ")

      
        
        29
        +

      
        
        30
        +	lip, err := getLocalIP()

      
        
        31
        +	if err != nil {

      
        
        32
        +		return "", err

      
        
        33
        +	}

      
        
        34
        +	_, _ = buf.WriteString(lip)

      
        
        35
        +

      
        
        36
        +	_ = buf.WriteByte('\n')

      
        
        37
        +	return buf.String(), nil

      
        
        38
        +}

      
        
        39
        +

      
        
        40
        +func getLocalIP() (string, error) {

      
        
        41
        +	addrs, err := net.InterfaceAddrs()

      
        
        42
        +	if err != nil {

      
        
        43
        +		return "", err

      
        
        44
        +	}

      
        
        45
        +	for _, addr := range addrs {

      
        
        46
        +		if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {

      
        
        47
        +			if ipnet.IP.To4() != nil {

      
        
        48
        +				return ipnet.IP.String(), nil

      
        
        49
        +			}

      
        
        50
        +		}

      
        
        51
        +	}

      
        
        52
        +	return "", errors.New("couldn't get local error")

      
        
        53
        +}

      
        
        54
        +

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

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

      
        
        57
        +	if err != nil {

      
        
        58
        +		return "", err

      
        
        59
        +	}

      
        
        60
        +	defer resp.Body.Close()

      
        
        61
        +

      
        
        62
        +	ip, err := io.ReadAll(resp.Body)

      
        
        63
        +	if err != nil {

      
        
        64
        +		return "", err

      
        
        65
        +	}

      
        
        66
        +	return strings.TrimSpace(string(ip)), nil

      
        
        67
        +}

      
A plumbing/shell/shell.go
···
        
        1
        +package shell

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"os/exec"

      
        
        6
        +	"strings"

      
        
        7
        +	"syscall"

      
        
        8
        +

      
        
        9
        +	"github.com/olexsmir/viye/core"

      
        
        10
        +)

      
        
        11
        +

      
        
        12
        +type Tool struct{}

      
        
        13
        +

      
        
        14
        +func (Tool) Name() string                 { return "shell($)" }

      
        
        15
        +func (Tool) Match(ctx *core.Context) bool { return isCmd(ctx.Path[0]) || isBGCmd(ctx.Path[0]) }

      
        
        16
        +func (Tool) Execute(ctx *core.Context) (string, error) {

      
        
        17
        +	switch {

      
        
        18
        +	case isCmd(ctx.Path[0]):

      
        
        19
        +		cmd := exec.Command("sh", "-c", strings.TrimPrefix(ctx.Path[0], "$ "))

      
        
        20
        +		cmd.Dir = ctx.Dir

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

      
        
        22
        +

      
        
        23
        +		lines := strings.Split(string(out), "\n")

      
        
        24
        +		for i, line := range lines {

      
        
        25
        +			if line != "" {

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

      
        
        27
        +			}

      
        
        28
        +		}

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

      
        
        30
        +

      
        
        31
        +	case isBGCmd(ctx.Path[0]):

      
        
        32
        +		cmd := exec.Command("sh", "-c", strings.TrimPrefix(ctx.Path[0], "$$ "))

      
        
        33
        +		cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        
        34
        +		cmd.Dir = ctx.Dir

      
        
        35
        +		if err := cmd.Start(); err != nil {

      
        
        36
        +			return "", err

      
        
        37
        +		}

      
        
        38
        +		return fmt.Sprintf("| pid: %d", cmd.Process.Pid), nil

      
        
        39
        +

      
        
        40
        +	default:

      
        
        41
        +		panic("shouldn't have paniced")

      
        
        42
        +	}

      
        
        43
        +}

      
        
        44
        +

      
        
        45
        +func isCmd(s string) bool   { return strings.HasPrefix(s, "$ ") }

      
        
        46
        +func isBGCmd(s string) bool { return strings.HasPrefix(s, "$$ ") }

      
A plumbing/url/url.go
···
        
        1
        +package url

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"strings"

      
        
        5
        +

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

      
        
        7
        +	"github.com/olexsmir/viye/core/osutil"

      
        
        8
        +)

      
        
        9
        +

      
        
        10
        +type Tool struct{}

      
        
        11
        +

      
        
        12
        +func (Tool) Name() string { return "url(http)" }

      
        
        13
        +func (Tool) Match(ctx *core.Context) bool {

      
        
        14
        +	return strings.HasPrefix(ctx.Path[0], "http://") || strings.HasPrefix(ctx.Path[0], "https://")

      
        
        15
        +}

      
        
        16
        +

      
        
        17
        +func (Tool) Execute(ctx *core.Context) (string, error) {

      
        
        18
        +	return "", osutil.Open(ctx.Path[0])

      
        
        19
        +}