fuzzyworld

template / commit

Merge branch 'patterns' into 'main'

bfbd125cb98928a4c88ac0e79e04bd330791d50f · Pavel S · 2022-09-23 00:15:12 +0000 · parents 4a7361b0a6aa ea896f474770

Merge branch 'patterns' into 'main'

Got patterns working

See merge request fuzzy-rp/template!1
 .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
+}