summaryrefslogtreecommitdiff
path: root/runner
diff options
context:
space:
mode:
authorewy <ewy0@protonmail.com>2026-04-14 16:37:17 +0200
committerewy <ewy0@protonmail.com>2026-04-14 16:37:17 +0200
commit45a297a8e526094e8fce6e2c5c0fd89b381d1765 (patch)
tree852ebc3a0112c94dc9726d0b27ab057bf6383660 /runner
i have to commit at some point!
Diffstat (limited to 'runner')
-rw-r--r--runner/base.go82
-rw-r--r--runner/python/filetarget.go51
-rw-r--r--runner/python/indexer.go44
-rw-r--r--runner/python/projtarget.go50
-rw-r--r--runner/python/runner.go86
-rw-r--r--runner/shell/hydrated.go17
-rw-r--r--runner/shell/shell.go176
-rw-r--r--runner/shell/shell_test.go22
-rw-r--r--runner/shell/target.go35
9 files changed, 563 insertions, 0 deletions
diff --git a/runner/base.go b/runner/base.go
new file mode 100644
index 0000000..7a6ce81
--- /dev/null
+++ b/runner/base.go
@@ -0,0 +1,82 @@
+package runner
+
+import (
+ "os/exec"
+ "pik/identity"
+ "pik/model"
+)
+
+type BaseTarget struct {
+ identity.Identity
+ MyTags model.Tags
+}
+
+func (t *BaseTarget) Tags() model.Tags {
+ return t.MyTags
+}
+
+func (t *BaseTarget) Matches(input string) bool {
+ return t.Identity.Is(input)
+}
+
+func (t *BaseTarget) ShortestId() string {
+ return t.Reduced
+}
+
+func Hydrated[T model.Target](in T) BaseHydration[T] {
+ return BaseHydration[T]{
+ BaseTarget: in,
+ }
+}
+
+type BaseHydration[T model.Target] struct {
+ BaseTarget T
+}
+
+func (b BaseHydration[T]) Matches(input string) bool {
+ return b.BaseTarget.Matches(input)
+}
+
+func (b BaseHydration[T]) Create(s *model.Source) *exec.Cmd {
+ return b.BaseTarget.Create(s)
+}
+
+func (b BaseHydration[T]) Sub() []string {
+ return b.BaseTarget.Sub()
+}
+
+func (b BaseHydration[T]) Label() string {
+ return b.BaseTarget.Label()
+}
+
+func (b BaseHydration[T]) Hydrate(src *model.Source) (model.HydratedTarget, error) {
+ return b, nil
+}
+
+func (b BaseHydration[T]) Visible() bool {
+ return b.BaseTarget.Visible()
+}
+
+func (b BaseHydration[T]) Tags() model.Tags {
+ return b.BaseTarget.Tags()
+}
+
+func (b BaseHydration[T]) ShortestId() string {
+ return b.BaseTarget.ShortestId()
+}
+
+func (b BaseHydration[T]) Icon() string {
+ return " "
+}
+
+func (b BaseHydration[T]) Description() string {
+ return "//TODO"
+}
+
+func (b BaseHydration[T]) Target() model.Target {
+ return b.BaseTarget
+}
+
+func (b BaseTarget) Visible() bool {
+ return b.Tags().Visible()
+}
diff --git a/runner/python/filetarget.go b/runner/python/filetarget.go
new file mode 100644
index 0000000..6793f4b
--- /dev/null
+++ b/runner/python/filetarget.go
@@ -0,0 +1,51 @@
+package python
+
+import (
+ "os/exec"
+ "path/filepath"
+ "pik/model"
+ "pik/runner"
+)
+
+type FileTarget struct {
+ runner.BaseTarget
+ File string
+}
+
+type HydratedFileTarget struct {
+ runner.BaseHydration[*FileTarget]
+}
+
+func (h *HydratedFileTarget) Icon() string {
+ return "\uE606"
+}
+
+func (p *FileTarget) Create(s *model.Source) *exec.Cmd {
+ var cmd []string
+ if Python.Uv != "" {
+ cmd = []string{Python.Uv, "run", "--", p.File}
+ } else if venv := Python.VenvFor(s); venv != "" {
+ cmd = []string{filepath.Join(s.Path, venv, "bin", "python3"), p.File}
+ } else {
+ sysPath, err := exec.LookPath("python3")
+ if err != nil {
+ return nil
+ }
+ cmd = []string{sysPath, p.File}
+ }
+ return exec.Command(cmd[0], cmd[1:]...)
+}
+
+func (p *FileTarget) Sub() []string {
+ return nil
+}
+
+func (p *FileTarget) Label() string {
+ return p.Full
+}
+
+func (p *FileTarget) Hydrate(src *model.Source) (model.HydratedTarget, error) {
+ return &HydratedFileTarget{
+ BaseHydration: runner.Hydrated(p),
+ }, nil
+}
diff --git a/runner/python/indexer.go b/runner/python/indexer.go
new file mode 100644
index 0000000..d0dd4e3
--- /dev/null
+++ b/runner/python/indexer.go
@@ -0,0 +1,44 @@
+package python
+
+import (
+ "github.com/pelletier/go-toml/v2"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "pik/model"
+)
+
+type Pyproj struct {
+ Project struct {
+ Scripts map[string]string
+ }
+}
+
+func (p python) Index(path string, f fs.FS, runners []model.Runner) ([]model.Target, error) {
+ for _, pt := range VenvPaths {
+ if stat, err := fs.Stat(f, filepath.Join(pt)); err == nil {
+ if stat.IsDir() {
+ p.Venvs[path] = filepath.Join(path, pt)
+ }
+ }
+ }
+ content, err := fs.ReadFile(f, "pyproject.toml")
+ if os.IsNotExist(err) {
+ return nil, nil
+ } else if err != nil {
+ return nil, err
+ }
+
+ pp := &Pyproj{}
+
+ err = toml.Unmarshal(content, pp)
+ if err != nil {
+ return nil, err
+ }
+
+ var targets = make([]model.Target, 0, len(pp.Project.Scripts))
+ for n, s := range pp.Project.Scripts {
+ targets = append(targets, Python.CreateProjTarget(n, s))
+ }
+ return targets, nil
+}
diff --git a/runner/python/projtarget.go b/runner/python/projtarget.go
new file mode 100644
index 0000000..1604948
--- /dev/null
+++ b/runner/python/projtarget.go
@@ -0,0 +1,50 @@
+package python
+
+import (
+ "os/exec"
+ "path/filepath"
+ "pik/model"
+ "pik/runner"
+)
+
+type ProjTarget struct {
+ runner.BaseTarget
+ Cmd string
+}
+
+type HydratedProjTarget struct {
+ runner.BaseHydration[*ProjTarget]
+}
+
+func (h *HydratedProjTarget) Icon() string {
+ return "\uE606"
+}
+
+func (h *HydratedProjTarget) Description() string {
+ //TODO implement me
+ return "//TODO"
+}
+
+func (p *ProjTarget) Create(s *model.Source) *exec.Cmd {
+ var cmd []string
+ if Python.Uv != "" {
+ cmd = []string{Python.Uv, "run", "--", p.Cmd}
+ } else if venv := Python.VenvFor(s); venv != "" {
+ cmd = []string{filepath.Join(s.Path, venv, "bin", "python"), p.Cmd}
+ }
+ return exec.Command(cmd[0], cmd[1:]...)
+}
+
+func (p *ProjTarget) Sub() []string {
+ return nil
+}
+
+func (p *ProjTarget) Label() string {
+ return p.Cmd
+}
+
+func (p *ProjTarget) Hydrate(src *model.Source) (model.HydratedTarget, error) {
+ return &HydratedProjTarget{
+ BaseHydration: runner.Hydrated(p),
+ }, nil
+}
diff --git a/runner/python/runner.go b/runner/python/runner.go
new file mode 100644
index 0000000..b06d8ab
--- /dev/null
+++ b/runner/python/runner.go
@@ -0,0 +1,86 @@
+package python
+
+import (
+ "errors"
+ "io/fs"
+ "os/exec"
+ "path/filepath"
+ "pik/identity"
+ "pik/model"
+ "pik/runner"
+)
+
+type python struct {
+ Venvs map[string]string
+ Uv string
+ System string
+}
+
+func (p python) Init() error {
+ uv, err := exec.LookPath("uv")
+ if err != nil && !errors.Is(err, exec.ErrNotFound) {
+ return err
+ }
+ p.Uv = uv
+ sys, err := exec.LookPath("python3")
+ if err == nil {
+ p.System = sys
+ }
+ return err
+}
+
+func (p python) Hydrate(target model.Target) (model.HydratedTarget, error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (p python) Wants(fs fs.FS, file string, entry fs.DirEntry) (bool, error) {
+ return !entry.IsDir() && filepath.Ext(entry.Name()) == ".py", nil
+}
+
+func (p python) VenvFor(src *model.Source) string {
+ venvPath := p.Venvs[src.Path]
+ if venvPath != "" {
+ return venvPath
+ }
+ return ""
+}
+
+func (p python) PyFor(src *model.Source) []string {
+ if p.Uv != "" {
+ return []string{p.Uv, "run", "--"}
+ }
+ if venv := p.VenvFor(src); venv != "" {
+ return []string{filepath.Join(src.Path, venv, "bin", "python")}
+ }
+ return nil
+}
+
+func (p python) CreateProjTarget(name string, cmd string) model.Target {
+ return &ProjTarget{
+ BaseTarget: runner.BaseTarget{
+ Identity: identity.New(name),
+ },
+ Cmd: cmd,
+ }
+}
+
+func (p python) CreateTarget(fs fs.FS, source string, file string, entry fs.DirEntry) (model.Target, error) {
+ _, filename := filepath.Split(file)
+ return &FileTarget{
+ BaseTarget: runner.BaseTarget{
+ Identity: identity.New(filename),
+ MyTags: model.TagsFromFilename(filename),
+ },
+ File: file,
+ }, nil
+}
+
+var VenvPaths = []string{
+ ".venv",
+ "venv",
+}
+
+var Python = &python{
+ Venvs: map[string]string{},
+}
diff --git a/runner/shell/hydrated.go b/runner/shell/hydrated.go
new file mode 100644
index 0000000..5a86d5a
--- /dev/null
+++ b/runner/shell/hydrated.go
@@ -0,0 +1,17 @@
+package shell
+
+import (
+ "pik/runner"
+)
+
+type HydratedShellTarget struct {
+ runner.BaseHydration[*ShellTarget]
+}
+
+func (h *HydratedShellTarget) Icon() string {
+ return "\uF489"
+}
+
+func (h *HydratedShellTarget) Description() string {
+ return "//TODO"
+}
diff --git a/runner/shell/shell.go b/runner/shell/shell.go
new file mode 100644
index 0000000..5333279
--- /dev/null
+++ b/runner/shell/shell.go
@@ -0,0 +1,176 @@
+package shell
+
+import (
+ "bufio"
+ "errors"
+ "io/fs"
+ "os/exec"
+ "path/filepath"
+ "pik/identity"
+ "pik/indexers/pikdex"
+ "pik/model"
+ "pik/runner"
+ "pik/spool"
+ "slices"
+ "strings"
+)
+
+//TODO: Clean up shell selection? Maybe default to bash?
+
+var NoContentError = errors.New("not enough content in target")
+var NoShellError = errors.New("could not find any shell interpreters")
+
+var ExtShellMap = map[string]string{
+ ".sh": "bash",
+ ".ps1": "powershell",
+}
+
+var Shells = []string{"bash", "bash.exe", "zsh", "fish", "powershell", "powershell.exe", "cmd.exe"}
+
+var Runner = &shell{
+ Locations: map[string]string{},
+}
+
+type shell struct {
+ Locations map[string]string
+}
+
+var WrongTargetError = errors.New("wrong target type")
+
+func (s *shell) Hydrate(target model.Target) (model.HydratedTarget, error) {
+ cast, ok := target.(*ShellTarget)
+ if !ok {
+ return nil, WrongTargetError
+ }
+ hyd := &HydratedShellTarget{BaseHydration: runner.Hydrated(cast)}
+ return hyd, nil
+}
+
+func (s *shell) Wants(f fs.FS, file string, entry fs.DirEntry) (bool, error) {
+ if entry != nil && entry.IsDir() {
+ return false, nil
+ }
+
+ fd, err := f.Open(file)
+ if err != nil {
+ return false, err
+ }
+ scanner := bufio.NewScanner(fd)
+ scanner.Split(bufio.ScanRunes)
+ if !scanner.Scan() {
+ return false, nil
+ }
+ txt := scanner.Text()
+ if txt == "#" { //
+ return true, nil
+ }
+ for k, _ := range ExtShellMap {
+ if strings.HasSuffix(file, k) {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func (s *shell) Find(shell string) (string, error) {
+ if s.Locations[shell] != "" {
+ return s.Locations[shell], nil
+ }
+
+ if p, err := exec.LookPath(shell); err == nil {
+ s.Locations[shell] = p
+ return shell, nil
+ } else {
+ return "", err
+ }
+}
+
+func (s *shell) CreateTarget(fs fs.FS, src string, file string, _ fs.DirEntry) (model.Target, error) {
+ shell, err := s.ShellFor(fs, file)
+ if err != nil {
+ return nil, err
+ }
+ _, filename := filepath.Split(file)
+ var sub []string
+ split := strings.Split(file, "/")
+ for _, p := range split {
+ if slices.Contains(pikdex.Roots, p) {
+ continue
+ }
+ if filename == p {
+ continue
+ }
+ sub = append(sub, p)
+ }
+ return &ShellTarget{
+ BaseTarget: runner.BaseTarget{
+ Identity: identity.New(filename),
+ MyTags: model.TagsFromFilename(filename),
+ },
+ Shell: shell,
+ Script: file,
+ SubValue: sub,
+ }, nil
+}
+
+func (s *shell) ShellFor(fs fs.FS, file string) (string, error) {
+
+ var shell, shebang string
+
+ // low-hanging fruit - indicative filename
+ if byFile := s.ShellByFilename(file); byFile != "" {
+ return byFile, nil
+ }
+
+ fd, err := fs.Open(file)
+ if err != nil {
+ return "", err
+ }
+ scanner := bufio.NewScanner(fd)
+ scanner.Split(bufio.ScanLines)
+ if !scanner.Scan() {
+ return "", NoContentError
+ }
+ txt := scanner.Text()
+ if strings.HasPrefix(txt, "#!") {
+ // shebang found
+ for _, potentialShell := range Shells {
+ if strings.Contains(txt, potentialShell) {
+ shebang = shell
+ if loc, err := s.Find(potentialShell); err == nil {
+ shell = loc
+ } else {
+ _, _ = spool.Warn("script has %s but could not find %s (%s)\n", shebang, potentialShell)
+ }
+ }
+ }
+ }
+
+ if shebang == "" {
+ // if no shebang, just send the first one we find
+ for _, s := range Shells {
+ if p, err := exec.LookPath(s); err != nil {
+ shell = p
+ }
+ }
+ }
+
+ if shell == "" {
+ return "", NoShellError
+ }
+
+ return shell, nil
+
+}
+
+func (s *shell) ShellByFilename(file string) string {
+ ext := filepath.Ext(file)
+ if ExtShellMap[ext] != "" {
+ sh, err := s.Find(ExtShellMap[ext])
+ if err == nil {
+ return sh
+ }
+ }
+
+ return ""
+}
diff --git a/runner/shell/shell_test.go b/runner/shell/shell_test.go
new file mode 100644
index 0000000..b28bd6c
--- /dev/null
+++ b/runner/shell/shell_test.go
@@ -0,0 +1,22 @@
+package shell
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestShell_ShellByFilename(t *testing.T) {
+ s := &shell{
+ Locations: map[string]string{"powershell": "/pws", "bash": "/bash"},
+ }
+ inputs := map[string]string{
+ "script.ps1": "/pws",
+ "script.sh": "/bash",
+ "asdf": "",
+ }
+
+ for k, v := range inputs {
+ sh := s.ShellByFilename(k)
+ assert.Equal(t, sh, v)
+ }
+}
diff --git a/runner/shell/target.go b/runner/shell/target.go
new file mode 100644
index 0000000..e823d41
--- /dev/null
+++ b/runner/shell/target.go
@@ -0,0 +1,35 @@
+package shell
+
+import (
+ "os/exec"
+ "path/filepath"
+ "pik/model"
+ "pik/runner"
+)
+
+type ShellTarget struct {
+ runner.BaseTarget
+ Shell string
+ Script string
+ SubValue []string
+}
+
+func (s *ShellTarget) String() string {
+ return s.Label()
+}
+
+func (s *ShellTarget) Hydrate(_ *model.Source) (model.HydratedTarget, error) {
+ return Runner.Hydrate(s)
+}
+
+func (s *ShellTarget) Sub() []string {
+ return s.SubValue
+}
+
+func (s *ShellTarget) Label() string {
+ return s.Identity.Full
+}
+
+func (s *ShellTarget) Create(src *model.Source) *exec.Cmd {
+ return exec.Command(s.Shell, filepath.Join(src.Path, s.Script))
+}