From 1859cbf02ded379dccdb4b05ea14cacc90552bc3 Mon Sep 17 00:00:00 2001 From: raj Date: Sun, 22 Oct 2023 01:12:58 +0530 Subject: [PATCH] init v0 --- config/main.go | 226 ++++++++++++++++++++++++++++++++++++++++ controller/tmux.go | 86 +++++++++++++++ go.mod | 38 +++++++ go.sum | 69 ++++++++++++ main.go | 21 ++++ project-manager/main.go | 128 +++++++++++++++++++++++ readme.md | 1 + ui/filepicker/main.go | 114 ++++++++++++++++++++ ui/table/main.go | 115 ++++++++++++++++++++ 9 files changed, 798 insertions(+) create mode 100644 config/main.go create mode 100644 controller/tmux.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 project-manager/main.go create mode 100644 readme.md create mode 100644 ui/filepicker/main.go create mode 100644 ui/table/main.go diff --git a/config/main.go b/config/main.go new file mode 100644 index 0000000..7ab5c14 --- /dev/null +++ b/config/main.go @@ -0,0 +1,226 @@ +package projectconfig + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "gopkg.in/yaml.v2" +) + +type Configuration struct { + Name string `yaml:"name"` + SessionName string `yaml:"session_name"` + WorkingDir string `yaml:"working_dir"` // New field for working directory + Tabs []struct { + Name string `yaml:"name"` + Commands []string `yaml:"commands"` + } `yaml:"tabs"` + LastOpened time.Time `yaml:"last_opened"` +} + +var configDir string + +func initConfigDir() string { + var configDirPath string + + if runtime.GOOS == "windows" { + usr, err := os.UserHomeDir() + if err != nil { + panic(err) + } + configDirPath = filepath.Join(usr, "AppData", "Local", "pee") + } else { + homeDir, err := os.UserHomeDir() + if err != nil { + panic(err) + } + configDirPath = filepath.Join(homeDir, ".config", "pee") + } + + _, err := os.Stat(configDirPath) + if os.IsNotExist(err) { + if err := os.MkdirAll(configDirPath, 0755); err != nil { + panic(err) + } + } + + return configDirPath +} + +func Init() { + configDir = initConfigDir() // Initialize the configDir variable. +} + +func ProjectConfigFilePath(projectName string) string { + return filepath.Join(configDir, projectName+".yml") +} + +func Load(filename string) (*Configuration, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var config Configuration + err = yaml.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + // config.LastOpened = time.Now() + + err = WriteConfigToFile(filename, &config) + if err != nil { + return nil, err + } + + return &config, nil +} + +func UpdateLastOpened(projectName string) error { + configFile := ProjectConfigFilePath(projectName) + + config, err := Load(configFile) + if err != nil { + return err + } + + config.LastOpened = time.Now() + + err = WriteConfigToFile(configFile, config) + if err != nil { + return err + } + + return nil +} + +func ListProjects() (map[string]*Configuration, error) { + projectConfigs := make(map[string]*Configuration) + + files, err := os.ReadDir(configDir) + if err != nil { + return nil, err + } + + for _, file := range files { + if file.IsDir() { + continue + } + projectName := strings.TrimSuffix(file.Name(), ".yml") + + projectConfigFile := filepath.Join(configDir, file.Name()) + config, err := Load(projectConfigFile) + if err != nil { + return nil, err + } + + projectConfigs[projectName] = config + } + + return projectConfigs, nil +} + +func GetProjectConfig(projectName string) (*Configuration, error) { + projectConfigFile := ProjectConfigFilePath(projectName) + config, err := Load(projectConfigFile) + if err != nil { + return nil, err + } + return config, nil +} + +func UpdateProjectConfig(projectName string, updatedConfig *Configuration) error { + configFile := ProjectConfigFilePath(projectName) + + err := WriteConfigToFile(configFile, updatedConfig) + if err != nil { + return err + } + + return nil +} + +func ProjectExists(projectName string) bool { + configFile := ProjectConfigFilePath(projectName) + + if _, err := os.Stat(configFile); err != nil { + return false + } + + return true +} + +func CreateProject(projectName, sessionName, workingDir string, tabs []struct { + Name string + Commands []string +}, +) (string, error) { + configFile := ProjectConfigFilePath(projectName) + + if _, err := os.Stat(configFile); err == nil { + return "", fmt.Errorf("Project with the name '%s' already exists", projectName) + } + + var tabsWithYAMLTags []struct { + Name string `yaml:"name"` + Commands []string `yaml:"commands"` + } + + for _, tab := range tabs { + tabWithYAMLTags := struct { + Name string `yaml:"name"` + Commands []string `yaml:"commands"` + }{ + Name: tab.Name, + Commands: tab.Commands, + } + tabsWithYAMLTags = append(tabsWithYAMLTags, tabWithYAMLTags) + } + + newConfig := &Configuration{ + Name: projectName, + SessionName: sessionName, + WorkingDir: workingDir, + Tabs: tabsWithYAMLTags, + LastOpened: time.Now(), + } + + err := WriteConfigToFile(configFile, newConfig) + if err != nil { + return "", err + } + + return configFile, nil +} + +func WriteConfigToFile(filename string, config *Configuration) error { + data, err := yaml.Marshal(config) + if err != nil { + return err + } + + indentedYAML := indentYAML(string(data), "") // Convert data to string + + err = os.WriteFile(filename, []byte(indentedYAML), 0644) + if err != nil { + return err + } + + return nil +} + +func indentYAML(yamlString, prefix string) string { + lines := strings.Split(yamlString, "\n") + indentedLines := make([]string, len(lines)) + + for i, line := range lines { + indentedLines[i] = prefix + line + } + + return strings.Join(indentedLines, "\n") +} diff --git a/controller/tmux.go b/controller/tmux.go new file mode 100644 index 0000000..1438777 --- /dev/null +++ b/controller/tmux.go @@ -0,0 +1,86 @@ +package controller + +import ( + "fmt" + "os/exec" + projectconfig "pee/config" + "strings" + + "github.com/charmbracelet/log" +) + +// CreateTmuxSession creates a tmux session based on the given Configuration. +func CreateTmuxSession(config *projectconfig.Configuration) error { + sessionName := config.SessionName + + // Check if the session exists + checkSessionCmd := exec.Command("tmux", "has-session", "-t", sessionName) + if err := checkSessionCmd.Run(); err == nil { + // If it exists, switch to the session + switchSessionCmd := exec.Command("tmux", "switch-client", "-t", sessionName) + if err := switchSessionCmd.Run(); err != nil { + return err + } + } else { + // If it doesn't exist, create the session + createSessionCmd := exec.Command("tmux", "new-session", "-d", "-s", sessionName) + if err := createSessionCmd.Run(); err != nil { + return err + } + log.Info("Ran command", "command", createSessionCmd.String()) + + // Change the working directory + changeDirCmd := exec.Command("tmux", "send-keys", "-t", sessionName, "cd "+config.WorkingDir, "Enter") + if err := changeDirCmd.Run(); err != nil { + return err + } + log.Info("Ran command", "command", changeDirCmd.String()) + + // Send commands to the session for the first tab + sendCommandsCmd := exec.Command("tmux", "send-keys", "-t", sessionName, strings.Join(config.Tabs[0].Commands, " && "), "Enter") + if err := sendCommandsCmd.Run(); err != nil { + return err + } + log.Info("Ran command", "command", sendCommandsCmd.String()) + // Rename the tab to the specified name + renameTabCmd := exec.Command("tmux", "rename-window", "-t", sessionName+":1", config.Tabs[0].Name) + if err := renameTabCmd.Run(); err != nil { + return err + } + + // Create and run commands for additional tabs + for i, tab := range config.Tabs[1:] { + windowName := fmt.Sprintf("%s:%d", sessionName, i+2) + createWindowCmd := exec.Command("tmux", "new-window", "-t", windowName, "-n", tab.Name) + if err := createWindowCmd.Run(); err != nil { + return err + } + log.Info("Ran command", "command", createWindowCmd.String()) + + changeDirCmd = exec.Command("tmux", "send-keys", "-t", windowName, "cd "+config.WorkingDir, "Enter") + if err := changeDirCmd.Run(); err != nil { + return err + } + log.Info("Ran command", "command", changeDirCmd.String()) + + sendCommandsCmd = exec.Command("tmux", "send-keys", "-t", windowName, strings.Join(tab.Commands, " && "), "Enter") + if err := sendCommandsCmd.Run(); err != nil { + return err + } + log.Info("Ran command", "command", sendCommandsCmd.String()) + } + + // Select the initial window and switch to the session + selectWindowCmd := exec.Command("tmux", "select-window", "-t", sessionName+":1") + if err := selectWindowCmd.Run(); err != nil { + return err + } + + switchSessionCmd := exec.Command("tmux", "switch-client", "-t", sessionName) + if err := switchSessionCmd.Run(); err != nil { + return err + } + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..58fe9e6 --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module pee + +go 1.19 + +require ( + github.com/alecthomas/kong v0.8.1 + github.com/spf13/cobra v1.7.0 +) + +require ( + github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/charmbracelet/log v0.2.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.2 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.3.8 // indirect + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..402aa37 --- /dev/null +++ b/go.sum @@ -0,0 +1,69 @@ +github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= +github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU= +github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/charmbracelet/log v0.2.5 h1:1yVvyKCKVV639RR4LIq1iy1Cs1AKxuNO+Hx2LJtk7Wc= +github.com/charmbracelet/log v0.2.5/go.mod h1:nQGK8tvc4pS9cvVEH/pWJiZ50eUq1aoXUOjGpXvdD0k= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..07f4a27 --- /dev/null +++ b/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" + projectconfig "pee/config" + projectmanager "pee/project-manager" +) + +func init() { + projectconfig.Init() + projectmanager.RootCmd.AddCommand(projectmanager.ListProjects) + projectmanager.RootCmd.AddCommand(projectmanager.InitCmd) +} + +func main() { + if err := projectmanager.RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/project-manager/main.go b/project-manager/main.go new file mode 100644 index 0000000..1350e23 --- /dev/null +++ b/project-manager/main.go @@ -0,0 +1,128 @@ +package projectmanager + +import ( + "fmt" + "os" + projectconfig "pee/config" + "pee/controller" + "pee/ui/filepicker" + "pee/ui/table" + + btable "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/log" + "github.com/spf13/cobra" +) + +var ListProjects = &cobra.Command{ + Use: "ls", + Short: "List all projects", + Run: func(cmd *cobra.Command, args []string) { + projects, err := projectconfig.ListProjects() + if err != nil { + fmt.Println(err) + return + } + columns := []btable.Column{ + {Title: "Name", Width: 20}, + {Title: "Session Name", Width: 20}, + {Title: "Working Dir", Width: 50}, + {Title: "Last Opened", Width: 20}, + } + var rows []btable.Row + + for projectName, config := range projects { + row := []string{ + projectName, + config.SessionName, + config.WorkingDir, + config.LastOpened.Format("2006-01-02 15:04:05"), + } + rows = append(rows, row) + } + selectedRow, action := table.Table(columns, rows) + if action == "edit" { + // print a vim command to open the config file + fmt.Println("vim", projectconfig.ProjectConfigFilePath(selectedRow[0])) + } + if action == "open" { + ExecuteProjectEnv(selectedRow[0]) + } + }, +} + +var RootCmd = &cobra.Command{ + Use: "pee", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ExecuteProjectEnv(args[0]) + }, +} + +func ExecuteProjectEnv(projectName string) { + config, err := projectconfig.GetProjectConfig(projectName) + if err != nil { + log.Error(err) + return + } + err = controller.CreateTmuxSession(config) + if err != nil { + log.Error(err) + return + } + projectconfig.UpdateLastOpened(projectName) + log.Info("Created tmux session", "name", config.SessionName) +} + +var InitCmd = &cobra.Command{ + Use: "init", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + projectName := args[0] + projectExists := projectconfig.ProjectExists(projectName) + if projectExists { + log.Warn("Project already exists", "name", projectName) + return + } + + selected, err := filepicker.FilePicker("Select your project dir", "Selected Dir: ") + if selected == "" { + log.Warn("No dir selected, aborting") + return + } + if err != nil { + log.Error(err) + return + } + log.Info("Selected", "work_dir", selected) + + sessionName := projectName + tabs := []struct { + Name string + Commands []string + }{ + { + Name: "editor", + Commands: []string{"echo 'command to open ur editor'"}, + }, + { + Name: "dev server", + Commands: []string{"echo 'command to start dev server'", "echo 'command to just initialize ur dependencies'"}, + }, + { + Name: "git", + Commands: []string{"echo 'command to open ur git client (use lazygit its amazing)'"}, + }, + } + logger := log.NewWithOptions(os.Stderr, log.Options{ + ReportCaller: false, + ReportTimestamp: false, + }) + ppath, err := projectconfig.CreateProject(projectName, sessionName, selected, tabs) + if err != nil { + logger.Error(err) + } else { + // logger.Info("Created Project", "path", ppath) + fmt.Println("Created Project", "setup your config by editing: ", ppath) + } + }, +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..875d007 --- /dev/null +++ b/readme.md @@ -0,0 +1 @@ +# Project Environment Executor diff --git a/ui/filepicker/main.go b/ui/filepicker/main.go new file mode 100644 index 0000000..eba1d18 --- /dev/null +++ b/ui/filepicker/main.go @@ -0,0 +1,114 @@ +package filepicker + +import ( + "errors" + "os" + "strings" + "time" + + "github.com/charmbracelet/bubbles/filepicker" + tea "github.com/charmbracelet/bubbletea" +) + +type model struct { + headerMessage string + selectedMessage string + filepicker filepicker.Model + selectedFile string + quitting bool + err error +} + +type clearErrorMsg struct{} + +func clearErrorAfter(t time.Duration) tea.Cmd { + return tea.Tick(t, func(_ time.Time) tea.Msg { + return clearErrorMsg{} + }) +} + +func (m model) Init() tea.Cmd { + return m.filepicker.Init() +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case " ": + m.quitting = true + return m, tea.Quit + case "q", "ctrl+c", "esc": + m.quitting = true + m.selectedFile = "" + m.filepicker.FileSelected = "" + return m, tea.Quit + } + case clearErrorMsg: + m.err = nil + } + + var cmd tea.Cmd + m.filepicker, cmd = m.filepicker.Update(msg) + + // Did the user select a file? + if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { + // Get the path of the selected file. + m.selectedFile = path + } + + // Did the user select a disabled file? + // This is only necessary to display an error to the user. + if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { + // Let's clear the selectedFile and display an error. + m.err = errors.New(path + " is not valid.") + m.selectedFile = "" + return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) + } + + return m, cmd +} + +func (m model) View() string { + if m.quitting { + return "" + } + var s strings.Builder + s.WriteString("\n ") + if m.err != nil { + s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) + } else if m.selectedFile == "" { + s.WriteString(m.headerMessage) + } else { + s.WriteString(m.selectedMessage + m.filepicker.Styles.Selected.Render(m.selectedFile) + " to select") + } + s.WriteString("\n\n" + m.filepicker.View() + "\n" + " or q to quit") + return s.String() +} + +func FilePicker(headerMessage string, selectedMessage string) (string, error) { + fp := filepicker.New() + // fp.AllowedTypes = []string{".mod", ".sum", ".go", ".txt", ".md"} + fp.DirAllowed = true + fp.FileAllowed = false + fp.CurrentDirectory, _ = os.Getwd() + fp.FileSelected, _ = os.Getwd() + + // Set default values for header and footer messages + if headerMessage == "" { + headerMessage = "Select file..." + } + if selectedMessage == "" { + selectedMessage = "Selected file: " + } + m := model{ + headerMessage: headerMessage, + selectedMessage: selectedMessage, + filepicker: fp, + selectedFile: fp.FileSelected, + } + tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run() + mm := tm.(model) + + return mm.selectedFile, err +} diff --git a/ui/table/main.go b/ui/table/main.go new file mode 100644 index 0000000..777becb --- /dev/null +++ b/ui/table/main.go @@ -0,0 +1,115 @@ +package table + +import ( + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" +) + +var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + +type keyMap struct { + Quit key.Binding + Edit key.Binding + Open key.Binding +} + +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Quit, k.Edit, k.Open} +} + +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Quit, k.Edit, k.Open}, // first column + } +} + +var keys = keyMap{ + Quit: key.NewBinding( + key.WithKeys("q", "esc", "ctrl+c"), + key.WithHelp("q", "quit"), + ), + Edit: key.NewBinding( + key.WithKeys("e"), + key.WithHelp("e", "edit selected project's configuration"), + ), + Open: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "open selected project"), + ), +} + +type model struct { + table table.Model + action string + help help.Model + keys keyMap +} + +func (m model) Init() tea.Cmd { return nil } + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc": + if m.table.Focused() { + m.table.Blur() + } else { + m.table.Focus() + } + case "q", "ctrl+c": + return m, tea.Quit + case "enter": + m.action = "open" + return m, tea.Quit + case "e": + m.action = "edit" + return m, tea.Quit + } + } + m.table, cmd = m.table.Update(msg) + return m, cmd +} + +func (m model) View() string { + helpView := m.help.View(m.keys) + return baseStyle.Render(m.table.View()) + "\n" + helpView + "\n\n" +} + +func Table(columns []table.Column, rows []table.Row) (table.Row, string) { + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(7), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + + m := model{t, "", help.New(), keys} + newModel, err := tea.NewProgram(m).Run() + if err != nil { + log.Fatal(err) + } + m = newModel.(model) + selected := rows[m.table.Cursor()] + + return selected, m.action +}