Git Repository
Stackyard
Stackyard scannt lokale Entwicklungsordner und bündelt Projekte, Git-Status, TODOs, Toolchains, Docker-Compose-Setups, Datei- und Launcher-Funktionen sowie Operations-Informationen in einer lokalen WebUI.
HTTPS
https://zanvex.de/git/stackyard.gitinternal/launcher/launcher.go
Zum Verzeichnispackage launcher
import (
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"stackyard/internal/config"
)
type Action string
const (
ActionVSCode Action = "vscode"
ActionSublime Action = "sublime"
ActionTerminal Action = "terminal"
ActionExplorer Action = "explorer"
)
type Service struct {
cfg config.LaunchersConfig
}
func New(cfg config.LaunchersConfig) Service {
return Service{cfg: cfg}
}
func (s Service) Open(action Action, projectPath string) error {
specs, err := s.commandSpecs(action, projectPath)
if err != nil {
return err
}
var lastErr error
for _, spec := range specs {
cmd := exec.Command(spec.name, spec.args...)
cmd.Dir = projectPath
if err := cmd.Start(); err != nil {
lastErr = err
continue
}
_ = cmd.Process.Release()
return nil
}
if lastErr != nil {
return fmt.Errorf("start %s: %w", action, lastErr)
}
return fmt.Errorf("start %s: no launcher candidates", action)
}
type commandSpec struct {
name string
args []string
}
func (s Service) commandSpecs(action Action, projectPath string) ([]commandSpec, error) {
switch action {
case ActionVSCode:
return editorCommandSpecs(s.cfg.VSCode, "code", projectPath)
case ActionSublime:
return editorCommandSpecs(s.cfg.Sublime, "subl", projectPath)
case ActionExplorer:
spec, err := platformCommand(s.cfg.Explorer, []string{projectPath}, explorerDefaults(projectPath))
if err != nil {
return nil, err
}
return []commandSpec{spec}, nil
case ActionTerminal:
spec, err := platformCommand(s.cfg.Terminal, []string{projectPath}, terminalDefaults(projectPath))
if err != nil {
return nil, err
}
return []commandSpec{spec}, nil
default:
return nil, fmt.Errorf("unsupported launcher action %q", action)
}
}
func configuredCommand(cfg config.LauncherConfig, trailingArgs []string) (commandSpec, error) {
if !cfg.Enabled {
return commandSpec{}, errors.New("launcher disabled")
}
if strings.TrimSpace(cfg.Command) == "" {
return commandSpec{}, errors.New("launcher command is empty")
}
parts := splitCommandLine(cfg.Command)
if len(parts) == 0 {
return commandSpec{}, errors.New("launcher command is empty")
}
return commandSpec{
name: parts[0],
args: append(parts[1:], trailingArgs...),
}, nil
}
func platformCommand(cfg config.LauncherConfig, trailingArgs, defaults []string) (commandSpec, error) {
if !cfg.Enabled {
return commandSpec{}, errors.New("launcher disabled")
}
if strings.TrimSpace(cfg.Command) != "" {
parts := splitCommandLine(cfg.Command)
if len(parts) == 0 {
return commandSpec{}, errors.New("launcher command is empty")
}
return commandSpec{name: parts[0], args: append(parts[1:], trailingArgs...)}, nil
}
if len(defaults) == 0 {
return commandSpec{}, errors.New("no launcher command configured for this platform")
}
return commandSpec{name: defaults[0], args: defaults[1:]}, nil
}
func explorerDefaults(projectPath string) []string {
if isWSL() {
winPath := toWindowsPath(projectPath)
return []string{"explorer.exe", winPath}
}
switch runtime.GOOS {
case "windows":
return []string{"explorer.exe", projectPath}
case "darwin":
return []string{"open", projectPath}
default:
return []string{"xdg-open", projectPath}
}
}
func terminalDefaults(projectPath string) []string {
if isWSL() {
winPath := toWindowsPath(projectPath)
return []string{"cmd.exe", "/c", "start", "", "cmd.exe", "/K", "cd", "/d", winPath}
}
switch runtime.GOOS {
case "windows":
return []string{"cmd.exe", "/c", "start", "", projectPath}
case "darwin":
return []string{"open", "-a", "Terminal", projectPath}
default:
return []string{"x-terminal-emulator", "--working-directory", projectPath}
}
}
func editorCommandSpecs(cfg config.LauncherConfig, defaultCommand, projectPath string) ([]commandSpec, error) {
if !cfg.Enabled {
return nil, errors.New("launcher disabled")
}
specs := make([]commandSpec, 0, 2)
if strings.TrimSpace(cfg.Command) != "" {
spec, err := configuredCommand(cfg, []string{projectPath})
if err != nil {
return nil, err
}
specs = append(specs, spec)
}
if isWSL() {
winPath := toWindowsPath(projectPath)
command := defaultCommand
if strings.TrimSpace(cfg.Command) != "" {
parts := splitCommandLine(cfg.Command)
if len(parts) > 0 {
command = toWindowsCommand(parts[0])
}
}
specs = append(specs, commandSpec{
name: "cmd.exe",
args: []string{"/c", "start", "", command, winPath},
})
}
if len(specs) == 0 {
specs = append(specs, commandSpec{name: defaultCommand, args: []string{projectPath}})
}
return specs, nil
}
func splitCommandLine(input string) []string {
var parts []string
var current strings.Builder
var quote rune
flush := func() {
if current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
}
}
for _, r := range input {
switch {
case quote != 0:
if r == quote {
quote = 0
} else {
current.WriteRune(r)
}
case r == '"' || r == '\'':
quote = r
case r == ' ' || r == '\t' || r == '\n':
flush()
default:
current.WriteRune(r)
}
}
flush()
return parts
}
func isWSL() bool {
return runtime.GOOS == "linux" && os.Getenv("WSL_DISTRO_NAME") != ""
}
func toWindowsPath(path string) string {
out, err := exec.Command("wslpath", "-w", path).Output()
if err != nil {
return path
}
return strings.TrimSpace(string(out))
}
func toWindowsCommand(command string) string {
if strings.HasPrefix(command, "/mnt/") {
return toWindowsPath(command)
}
return command
}