template / commit
Merge branch 'patterns' into 'main'
.vscode/launch.json | 46 +++++++++- .vscode/tasks.json | 17 ++++ config.go | 49 +++++++++-- go.mod | 1 + go.sum | 2 + main.go | 76 +++++++++++------ new.go | 236 ++++++++++++++++++++++++++++++++++++++++++++++++++++ utils.go | 17 ++++ 8 files changed, 413 insertions(+), 31 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 896dd01..8ed2713 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,28 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch Package", + "name": "Delete ~/.template.yaml", + "type": "PowerShell", + "request": "launch", + "script": "rm ~/.template.yaml -ErrorAction SilentlyContinue", + "cwd": "${workspaceFolder}" + }, + { + "name": "Install template", + "type": "PowerShell", + "request": "launch", + "script": "go install", + "cwd": "${workspaceFolder}" + }, + { + "name": "Run: template", + "type": "go", + "request": "launch", + "mode": "auto", + "program": ".", + }, + { + "name": "Run: template blog", "type": "go", "request": "launch", "mode": "auto", @@ -13,6 +34,29 @@ "args": [ "blog" ] + }, + { + "name": "Run: template work", + "type": "go", + "request": "launch", + "mode": "auto", + "program": ".", + "args": [ + "work" + ] + }, + { + "name": "Run: template --profile work --name testProj", + "type": "go", + "request": "launch", + "mode": "auto", + "program": ".", + "args": [ + "--profile", + "work", + "--name", + "testProj" + ] } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2e7bb7f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "remove-template-yaml", + "type": "shell", + "command": "rm ~/.template.yaml -ErrorAction SilentlyContinue", + "options": { + "shell": { + "executable": "powershell.exe" + } + } + } + ] +} \ No newline at end of file diff --git a/config.go b/config.go index 115748e..b1991e9 100644 --- a/config.go +++ b/config.go @@ -14,8 +14,13 @@ const ( ) type Profile struct { - Path string - Type ProfileType + IsDefault bool + Path string + Type ProfileType + NamePatterns []string + PostScript []string + + // FileTemplatePath string } type Config struct { @@ -25,17 +30,51 @@ type Config struct { func defaultConfig() Config { return Config{ Profiles: map[string]Profile{ - "default": { - Path: "C:/DailyNotes", - Type: FileType, + "daily": { + IsDefault: true, + Path: "C:\\work\\daily-notes", + Type: FileType, + NamePatterns: []string{ + "{{$index := getUniqueIndex}}" + + "{{.Year}}-{{.Month}}-{{.Day}}" + + "{{if (gt $index 0)}}-{{$index}}{{end}}" + + ".md", + }, + PostScript: []string{ + "code", + ".", + "{{.Path}}", + }, }, "blog": { Path: "~/Dropbox/Blog", Type: FileType, + NamePatterns: []string{ + "{{$index := getUniqueIndex}}" + + "{{.Year}}-{{.Month}}-{{.Day}}" + + "{{if (gt $index 0)}}-{{$index}}{{end}}" + + "{{if .ProjectName}}-{{.ProjectName}}{{end}}" + + ".md", + }, + PostScript: []string{ + "code", + ".", + "{{.Name}}", + }, }, "work": { Path: "C:/work", Type: FolderType, + NamePatterns: []string{ + "{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", + "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", + "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", + "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", + }, + PostScript: []string{ + "explorer", + "{{.Path}}", + }, }, }, } diff --git a/go.mod b/go.mod index fb6c015..fb5860f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/urfave/cli/v2 v2.16.3 + golang.org/x/exp v0.0.0-20220921164117-439092de6870 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 6fad046..059a9a7 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= +golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index c76a9bd..a8ad254 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,14 @@ import ( "fmt" "log" "os" - "path/filepath" "github.com/urfave/cli/v2" // imports as package "cli" ) func main() { - defaultProfileName := "default" + configFilePath := getFullPath("~/.template.yaml") var profileName string + var projectName string app := &cli.App{ Name: "template", @@ -21,30 +21,26 @@ func main() { &cli.StringFlag{ Name: "profile", Aliases: []string{"p"}, - Value: defaultProfileName, + Value: "", Usage: "`PROFILE` to use for the template", Destination: &profileName, + // Value: defaultProfileName, // Required: true, // EnvVars: []string{"TEMPLATE_PROFILE"}, }, + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "name of the project to create", + Value: "", + Destination: &projectName, + }, }, Action: func(context *cli.Context) error { - if profileName == defaultProfileName && context.Args().Len() > 0 { - profileName = context.Args().Get(0) - } - message := fmt.Sprintf("Profile: %v", profileName) - fmt.Println(message) - - userProfile, err := os.UserHomeDir() - if err != nil { - processError(err, "Getting user home dir") - } - - configFilePath := filepath.Join(userProfile, ".template.yaml") + // try and load config config, err := loadConfig(configFilePath) if err != nil { - fmt.Printf("No config present at '%v', creating and using sample config", configFilePath) - fmt.Println() + fmt.Printf("No config present at '%v', creating and using sample config\n", configFilePath) // create config and save err := saveConfig(configFilePath, defaultConfig()) @@ -58,16 +54,46 @@ func main() { processError(err, "Error while loading") } } else { - fmt.Printf("Config loaded from '%v'", configFilePath) - fmt.Println() + // log successful load + fmt.Printf("Config loaded from '%v'\n", configFilePath) } - profile, ok := config.Profiles[profileName] - if ok { - fmt.Println("Found profile!") - fmt.Println(profile) - } else { - processError(fmt.Errorf("Unable to find profile '%v'", profileName), "Error finding profile") + // read args + args := context.Args() + + // get profile name from args + // if not set explicitly, use first arg, if present + if profileName == "" && args.Len() > 0 { + profileName = args.Get(0) + } + fmt.Println("Profile:", profileName) + + // get project name from args + // if not set explicitly, use second arg, if present + if projectName == "" && args.Len() > 1 { + projectName = args.Get(1) + } + if projectName != "" { + fmt.Println("Project name:", projectName) + } + + // find profile + profile, err := findProfile(config, profileName) + if err != nil { + processError(err, "Error while finding profile") + } + + // change cwd + path := getFullPath(profile.Path) + err = os.Chdir(path) + if err != nil { + processError(err, fmt.Sprintf("Error while changing directory to '%v'", path)) + } + + // process profile + err = processProfile(profile, projectName) + if err != nil { + processError(err, "Error processing profile") } return nil diff --git a/new.go b/new.go new file mode 100644 index 0000000..4033292 --- /dev/null +++ b/new.go @@ -0,0 +1,236 @@ +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" + "time" + + "golang.org/x/exp/slices" +) + +func findProfile(config Config, name string) (Profile, error) { + if name == "" { + // use default profile + for _, p := range config.Profiles { + if p.IsDefault { + return p, nil + } + } + } else { + // find profile by name + profile, ok := config.Profiles[name] + if ok { + return profile, nil + } else { + return Profile{}, fmt.Errorf("Unable to find profile '%v'", name) + } + } + return Profile{}, errors.New("Couldn't find profile") +} + +func processProfile(profile Profile, name string) error { + // validate profile + if profile.NamePatterns == nil || len(profile.NamePatterns) == 0 { + return errors.New("No name patterns specified") + } + + // get current directory + cwd, err := os.Getwd() + if err != nil { + return err + } + + // get all entities in current directory + files, err := os.ReadDir(cwd) + if err != nil { + return err + } + + // determine if we're dealing with files or directories + var useDirs bool + switch profile.Type { + case FileType: + useDirs = false + case FolderType: + useDirs = true + default: + return fmt.Errorf("Unknown profile type: %v", profile.Type) + } + + // filter entities to just files or directories + var entities []string + for _, file := range files { + isDir := file.IsDir() + if isDir == useDirs { + entities = append(entities, file.Name()) + } + } + + // generate a new name based on profile and existing entities + newName, err := createNewName(name, profile, entities) + if err != nil { + return err + } + + fullPath := filepath.Join(cwd, newName) + + // create new entity + if newName == "" { + return errors.New("Couldn't find a good new name") + } else { + if useDirs { + fmt.Printf("Creating directory: %v\n", fullPath) + err = os.Mkdir(fullPath, os.ModePerm) + if err != nil { + return err + } + } else { + fmt.Printf("Creating file: %v\n", fullPath) + os.WriteFile(fullPath, []byte{}, os.ModePerm) + } + } + + // execute PostScript, if present + postScripts, err := createPostScriptArray(profile, newName, fullPath) + if err != nil { + return err + } + if postScripts != nil && len(postScripts) > 0 { + allArgs := []string{"/C"} + allArgs = append(allArgs, postScripts...) + debugProfileScript := fmt.Sprintf("cmd %v", strings.Trim(fmt.Sprint(allArgs), "[]")) + fmt.Printf("Executing post script: %v \n", debugProfileScript) + cmd := exec.Command("cmd", allArgs...) + if err := cmd.Run(); err != nil { + // post scripts tend to fail (TODO: investigate why), so for now we'll just log the error + fmt.Printf("Error executing post script '%v': %v\n", debugProfileScript, err) + } + } + + return nil +} + +type NamePatternData struct { + Year, Month, Day, Hour, Minute, Second string + ProjectName string + Index int +} +type PostScriptPatternData struct { + Name string + Path string +} + +func createNewName(name string, profile Profile, existingEntities []string) (string, error) { + now := time.Now() + patternInfo := createNamePatternData(now, name) + newName, err := createNewNameForProfile(profile, patternInfo, existingEntities) + if err != nil { + return "", err + } else { + return newName, nil + } +} + +func createNamePatternData(now time.Time, projectName string) NamePatternData { + patternInfo := NamePatternData{ + Year: now.Format("2006"), + Month: now.Format("01"), + Day: now.Format("02"), + Hour: now.Format("15"), + Minute: now.Format("04"), + Second: now.Format("05"), + ProjectName: projectName, + Index: 0, + } + + return patternInfo +} + +func createNewNameForProfile(profile Profile, patternData NamePatternData, existingEntities []string) (string, error) { + for index, namePattern := range profile.NamePatterns { + + startingIndex := 0 + invokeCount := 0 + funcMap := template.FuncMap{ + "getUniqueIndex": func() int { + invokeCount++ + return startingIndex + }, + } + template := template.New("template-name").Funcs(funcMap) + + for { + + template, err := template.Parse(namePattern) + if err != nil { + return "", err + } + + sb := new(strings.Builder) + err = template.Execute(sb, patternData) + if err != nil { + return "", err + } + + newName := sb.String() + alreadyExists := slices.Contains(existingEntities, newName) + useIndex := invokeCount > 0 + isLastPattern := index == len(profile.NamePatterns)-1 + + // if no conflict, use this name + if !alreadyExists { + return newName, nil + } + // if index is not being used (template did not invoke getUniqueIndex) + // then we're done + if !useIndex { + break + } + + // if getUniqueIndex was used, but this isn't the last pattern, report an error + if useIndex && !isLastPattern { + return "", errors.New("Cannot use function getUniqueIndex in a pattern that is not the last pattern") + } + + startingIndex++ + } + } + return "", errors.New("Couldn't find a new name") +} + +func createPostScriptArray(profile Profile, name string, path string) ([]string, error) { + array := make([]string, 0) + + if profile.PostScript == nil || len(profile.PostScript) == 0 { + return nil, nil + } + + patternData := PostScriptPatternData{ + Name: name, + Path: path, + } + + for _, postScriptPattern := range profile.PostScript { + + template, err := template.New("template-name").Parse(postScriptPattern) + if err != nil { + return nil, err + } + + sb := new(strings.Builder) + err = template.Execute(sb, patternData) + if err != nil { + return nil, err + } + + postScript := sb.String() + array = append(array, postScript) + } + + return array, nil +} diff --git a/utils.go b/utils.go index d76a032..ceb6403 100644 --- a/utils.go +++ b/utils.go @@ -3,6 +3,9 @@ package main import ( "fmt" "os" + "os/user" + "path/filepath" + "strings" ) func processError(err error, message string) { @@ -15,3 +18,17 @@ func processError(err error, message string) { os.Exit(2) } + +func getFullPath(path string) string { + usr, _ := user.Current() + dir := usr.HomeDir + var newPath string + if path == "~" { + newPath = dir + } else if strings.HasPrefix(path, "~/") { + newPath = filepath.Join(dir, path[2:]) + } else { + newPath = path + } + return newPath +}