main.go (4399B)
1 // Command project-manager is a tool to manage projects. 2 // Usage: 3 // 4 // project-manager [flags] 5 // 6 // The flags are: 7 // 8 // -config string 9 // Path to config file (default "~/.config/ybkimm/project-manager.json") 10 // 11 // Configuration example: 12 // 13 // { "project-directory": "/path/to/projects", "active-directory": "/path/to/active/projects" } 14 package main 15 16 import ( 17 "flag" 18 "fmt" 19 "os" 20 "path/filepath" 21 "syscall" 22 "unsafe" 23 24 tea "github.com/charmbracelet/bubbletea" 25 "golang.org/x/sys/unix" 26 ) 27 28 var ( 29 flagConfig = flag.String( 30 "config", 31 "~/.config/ybkimm/project-manager.json", 32 "Path to config file", 33 ) 34 ) 35 36 func main() { 37 flag.Parse() 38 39 configPath, err := expandPath(*flagConfig) 40 if err != nil { 41 panic(err) 42 } 43 44 config, err := loadConfig(configPath) 45 if err != nil { 46 panic(err) 47 } 48 49 projects, err := GetProjects(config) 50 if err != nil { 51 panic(err) 52 } 53 54 p := tea.NewProgram(newAppModel(projects)) 55 56 m, err := run(p) 57 if err != nil { 58 panic(err) 59 } 60 61 if m.shouldSave { 62 fmt.Println("Saving changes...") 63 if err := saveChanges(config, m); err != nil { 64 panic(err) 65 } 66 fmt.Println("Done.") 67 } 68 69 if len(m.selectedAction) > 0 && m.selectedProject != nil { 70 projectPath := filepath.Join(config.ProjectDirectory, m.selectedProject.Name) 71 action := fmt.Sprintf(m.selectedAction, projectPath) 72 73 err := executeAction(action) 74 if err != nil { 75 panic(err) 76 } 77 } 78 } 79 80 func run(p *tea.Program) (appModel, error) { 81 m, err := p.Run() 82 if err != nil { 83 return appModel{}, err 84 } 85 86 return m.(appModel), nil 87 } 88 89 func saveChanges(config *Config, m appModel) error { 90 for _, project := range m.projects { 91 if project.IsActive { 92 if err := activateProject(config, project); err != nil { 93 return err 94 } 95 } else { 96 if err := deactivateProject(config, project); err != nil { 97 return err 98 } 99 } 100 } 101 102 return nil 103 } 104 105 func getPaths(config *Config, project string) (projectPath, activePath, archivePath string) { 106 projectPath = filepath.Join(config.ProjectDirectory, project) 107 activePath = filepath.Join(config.ActiveDirectory, project) 108 archivePath = filepath.Join(config.ArchivedDirectory, project) 109 110 return 111 } 112 113 func activateProject(config *Config, project *Project) error { 114 projectPath, activePath, archivePath := getPaths(config, project.Name) 115 116 err := removeSymlink(archivePath) 117 if err != nil { 118 return err 119 } 120 121 return createSymlink(projectPath, activePath) 122 } 123 124 func deactivateProject(config *Config, project *Project) error { 125 projectPath, activePath, archivePath := getPaths(config, project.Name) 126 127 err := removeSymlink(activePath) 128 if err != nil { 129 return err 130 } 131 132 return createSymlink(projectPath, archivePath) 133 } 134 135 // createSymlink creates symlink. 136 // if symlink is already exists, ignore. 137 func createSymlink(src, dst string) error { 138 if _, err := os.Stat(dst); os.IsNotExist(err) { 139 fmt.Println("Creating symlink:", src, "->", dst) 140 return os.Symlink(src, dst) 141 } else if err != nil { 142 return err 143 } 144 145 return nil 146 } 147 148 // removeSymlink removes symlink. 149 // if symlink is not exists, ignore. 150 // if path is not symlink, it warns and returns nil. 151 func removeSymlink(path string) error { 152 stat, err := os.Lstat(path) 153 if os.IsNotExist(err) { 154 return nil 155 } else if err != nil { 156 return err 157 } 158 159 isSymlink := stat.Mode()&os.ModeSymlink != 0 160 if isSymlink { 161 fmt.Println("Removing symlink:", path) 162 return os.Remove(path) 163 } 164 165 fmt.Println("Warning: Not symlink:", path) 166 return nil 167 } 168 169 func executeAction(action string) error { 170 fmt.Println("Executing action:", action) 171 172 // Get the current terminal settings. 173 termios, err := unix.IoctlGetTermios(syscall.Stdin, unix.TIOCGETA) 174 if err != nil { 175 return err 176 } 177 178 // Set the new terminal settings to be the same as the current settings. 179 newTermios := *termios 180 181 // Enable canonical mode, which sets the terminal to send complete lines of input 182 // to the shell process. 183 newTermios.Lflag |= syscall.ICANON 184 185 // Disable echo mode, which stops the terminal from echoing input characters. 186 newTermios.Lflag &^= syscall.ECHO 187 188 // Set the new terminal settings. 189 _, _, errno := syscall.Syscall( 190 syscall.SYS_IOCTL, 191 uintptr(syscall.Stdin), 192 uintptr(syscall.TIOCSETA), 193 uintptr(unsafe.Pointer(&newTermios)), 194 ) 195 if errno != 0 { 196 return errno 197 } 198 199 // Replace the current process with the shell process. 200 err = syscall.Exec("/bin/sh", []string{"/bin/sh", "-c", action}, os.Environ()) 201 if err != nil { 202 return err 203 } 204 205 return nil 206 }