project-manager

Project directory management for macOS
git clone git://git.lair.cx/project-manager
Log | Files | Refs | README | LICENSE

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 }