diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..e89fbf9 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake "git+https://code.oliverdavies.uk/opdavies/dev-shells#go" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4ceeaf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/zet diff --git a/build b/build new file mode 100755 index 0000000..af2e46b --- /dev/null +++ b/build @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "Building..." + +go build -o zet main.go + +echo "Done." diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 0000000..e5c2fcc --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "strings" + + "github.com/spf13/cobra" + + "code.oliverdavies.uk/opdavies/cmd-zet/internal/lib" +) + +var createCmd = &cobra.Command{ + Use: "create", + Aliases: []string{"c", "n", "new"}, + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + title := "" + + if len(args) > 0 { + title = strings.Join(args, " ") + } + + lib.CreateZet(title) + }, +} diff --git a/cmd/edit.go b/cmd/edit.go new file mode 100644 index 0000000..ea5fba5 --- /dev/null +++ b/cmd/edit.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "fmt" + "os" + "strconv" + + "github.com/spf13/cobra" + + "code.oliverdavies.uk/opdavies/cmd-zet/internal/lib" +) + +var editCmd = &cobra.Command{ + Use: "edit", + Aliases: []string{"e"}, + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + fmt.Println("Error: No id provided") + os.Exit(1) + } + + idInt, err := strconv.Atoi(args[0]) + if err != nil { + os.Exit(1) + } + + lib.EditZet(idInt) + }, +} diff --git a/cmd/find.go b/cmd/find.go new file mode 100644 index 0000000..817b257 --- /dev/null +++ b/cmd/find.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "code.oliverdavies.uk/opdavies/cmd-zet/internal/lib" +) + +var findCmd = &cobra.Command{ + Use: "find", + Aliases: []string{"f", "s", "search"}, + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + fmt.Println("No query") + + os.Exit(1) + } + + zets := lib.SearchZets(args[0]) + + lib.ParseZetList(zets) + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..a3d98ad --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "zet", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(editCmd) + rootCmd.AddCommand(findCmd) + rootCmd.AddCommand(titlesCmd) + rootCmd.AddCommand(viewCmd) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.zet.yaml)") +} diff --git a/cmd/titles.go b/cmd/titles.go new file mode 100644 index 0000000..048253b --- /dev/null +++ b/cmd/titles.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "code.oliverdavies.uk/opdavies/cmd-zet/internal/lib" +) + +var titlesCmd = &cobra.Command{ + Use: "titles", + Aliases: []string{"t"}, + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + zets := lib.GetAllZets() + + lib.ParseZetList(zets) + }, +} diff --git a/cmd/view.go b/cmd/view.go new file mode 100644 index 0000000..a06846b --- /dev/null +++ b/cmd/view.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + "os" + "strconv" + + "github.com/spf13/cobra" + + "code.oliverdavies.uk/opdavies/cmd-zet/internal/lib" +) + +var viewCmd = &cobra.Command{ + Use: "view", + Aliases: []string{"v"}, + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + fmt.Println("Error: No id provided") + os.Exit(1) + } + + idInt, err := strconv.Atoi(args[0]) + + if err != nil { + os.Exit(1) + } + + fmt.Println(lib.ViewZet(idInt)) + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bcaf3a4 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module code.oliverdavies.uk/opdavies/cmd-zet + +go 1.24.6 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..989827e --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/lib/config.go b/internal/lib/config.go new file mode 100644 index 0000000..9d482ae --- /dev/null +++ b/internal/lib/config.go @@ -0,0 +1,5 @@ +package lib + +func GetZetDir() string { + return "/home/opdavies/Documents/zet" +} diff --git a/internal/lib/file.go b/internal/lib/file.go new file mode 100644 index 0000000..77781e7 --- /dev/null +++ b/internal/lib/file.go @@ -0,0 +1,37 @@ +package lib + +import ( + "fmt" + "os" +) + +func EditFile(filePath string) { + if _, err := os.Stat(filePath); os.IsNotExist(err) { + fmt.Printf("Error: The file for path '%s' was not found\n", filePath) + os.Exit(1) + } + + editor := os.Getenv("EDITOR") + + err := Exec(editor, filePath) + + if err != nil { + fmt.Println(err) + } +} + +func ViewFile(filePath string) string { + if _, err := os.Stat(filePath); os.IsNotExist(err) { + fmt.Printf("Error: The file for path '%s' was not found\n", filePath) + os.Exit(1) + } + + content, err := os.ReadFile(filePath) + + if err != nil { + fmt.Println("Error opening the file:", err) + os.Exit(1) + } + + return string(content) +} diff --git a/internal/lib/git.go b/internal/lib/git.go new file mode 100644 index 0000000..4ffb8a2 --- /dev/null +++ b/internal/lib/git.go @@ -0,0 +1,35 @@ +package lib + +import ( + "os" + "os/exec" + "strconv" +) + +func CommitZettel(id int, title string) { + idString := strconv.Itoa(id) + + runGitCommand("add", idString) + runGitCommand("commit", "-m", title) + runGitCommand("push") +} + +func execGitCommand(parts ...string) (string, error) { + args := append([]string{"-C", GetZetDir()}, parts...) + command := exec.Command("git", args...) + + output, err := command.CombinedOutput() + + return string(output), err +} + +func runGitCommand(parts ...string) { + args := append([]string{"-C", GetZetDir()}, parts...) + command := exec.Command("git", args...) + + command.Stderr = os.Stderr + command.Stdin = os.Stdin + command.Stdout = os.Stdout + + command.Run() +} diff --git a/internal/lib/run.go b/internal/lib/run.go new file mode 100644 index 0000000..48318ff --- /dev/null +++ b/internal/lib/run.go @@ -0,0 +1,22 @@ +package lib + +import ( + "fmt" + "os" + "os/exec" +) + +func Exec(args ...string) error { + path, err := exec.LookPath(args[0]) + + if err != nil { + fmt.Println(err) + } + + cmd := exec.Command(path, args[1:]...) + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + + return cmd.Run() +} diff --git a/internal/lib/zet.go b/internal/lib/zet.go new file mode 100644 index 0000000..2fdfeb8 --- /dev/null +++ b/internal/lib/zet.go @@ -0,0 +1,195 @@ +package lib + +import ( + "bufio" + "fmt" + "log" + "os" + "os/exec" + "path" + "regexp" + "sort" + "strconv" + "strings" +) + +func CreateZet(title string) { + zid := newZid() + + fmt.Println(title, zid) + + path := path.Join(GetZetDir(), strconv.Itoa(zid)) + + os.Mkdir(path, 0750) + + filePath := fmt.Sprintf("%s/index.adoc", path) + + file, err := os.Create(filePath) + + if err != nil { + log.Fatal(err) + } + + defer file.Close() + + _, err = fmt.Fprintf(file, "= %s", title) + + if err != nil { + log.Fatal(err) + } + + EditFile(filePath) + + onSave(zid) +} + +func EditZet(id int) { + zetPath := path.Join(GetZetDir(), strconv.Itoa(id), "index.adoc") + + EditFile(zetPath) + + onSave(id) +} + +func GetAllZets() []int { + zets, err := execGitCommand("ls-files") + + if err != nil { + log.Println(err) + } + + re := regexp.MustCompile(`[0-9]+`) + matches := re.FindAllString(zets, -1) + + sort.Strings(matches) + + ids := make(map[int]struct{}) + for _, id := range matches { + num, err := strconv.Atoi(id) + + if err == nil { + ids[num] = struct{}{} + } + } + + var sorted []int + for num := range ids { + sorted = append(sorted, num) + } + + sort.Ints(sorted) + + return sorted +} + +func SearchZets(query string) []int { + zets, err := execGitCommand("grep", "-i", "--name-only", "--word-regex", query) + + if err != nil { + fmt.Printf("No matches found for %s.\n", query) + + os.Exit(1) + } + + re := regexp.MustCompile(`[0-9]+`) + matches := re.FindAllString(zets, -1) + + sort.Strings(matches) + + ids := make(map[int]struct{}) + for _, id := range matches { + num, err := strconv.Atoi(id) + + if err == nil { + ids[num] = struct{}{} + } + } + + var sorted []int + for num := range ids { + sorted = append(sorted, num) + } + + sort.Ints(sorted) + + return sorted +} + +func ParseZetList(ids []int) []string { + var lines []string + + green := "\033[32m" + reset := "\033[0m" + + for _, num := range ids { + line := fmt.Sprintf("%s%s%s %s", green, strconv.Itoa(num), reset, getTitle(num)) + + fmt.Println(line) + + lines = append(lines, line) + } + + return lines +} + +func ViewZet(id int) string { + zetPath := path.Join(GetZetDir(), strconv.Itoa(id), "index.adoc") + + return ViewFile(zetPath) +} + +func getTitle(id int) string { + return getTitleFromFile(path.Join(strconv.Itoa(id), "index.adoc")) +} + +func getTitleFromFile(filePath string) string { + filePath = path.Join(GetZetDir(), filePath) + + file, err := os.Open(filePath) + + if err != nil { + fmt.Println("Error opening file:", err) + + return "" + } + + defer file.Close() + + scanner := bufio.NewScanner(file) + + if scanner.Scan() { + text := scanner.Text() + + return strings.TrimPrefix(text, "= ") + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading file:", err) + } + + return "" +} + +func newZid() int { + cmd := exec.Command("ls", GetZetDir()) + output, _ := cmd.CombinedOutput() + + zets := strings.Split(string(output), "\n") + + var zetCount int + + for _, zet := range zets { + num, err := strconv.Atoi(zet) + if err == nil && num > zetCount { + zetCount = num + } + } + + return zetCount + 1 +} + +func onSave(id int) { + title := getTitle(id) + + CommitZettel(id, title) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..7b82f30 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "code.oliverdavies.uk/opdavies/cmd-zet/cmd" + +func main() { + cmd.Execute() +} diff --git a/watch b/watch new file mode 100755 index 0000000..fc894da --- /dev/null +++ b/watch @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -euo pipefail + +onchange "**/*.go" "./build && echo "" && ./zet $*" diff --git a/zet b/zet deleted file mode 100755 index 19633c4..0000000 --- a/zet +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env bash - -set -o pipefail - -IFS= read -rd '' USAGE < "$ZID/index.adoc" - edit_file "$ZID/index.adoc" - on_save "$ZID" -} - -delete_zettel() { - [[ -d "$ZID" ]] && rm -fr "$ZID" - commit_zettel "$ZID" -} - -edit_file() { - "$EDITOR" "$1" -} - -edit_zet() { - edit_file "$1/index.adoc" - on_save "$1" -} - -generate_links() { - echo "$1" | while IFS= read -r line; do - id="${line%% *}" - title="${line#* }" - echo "* link:../${id}/index.adoc[${title}]" - done -} - -get_latest_zettel() { - find . -maxdepth 1 -type d -name '[0-9]*' -printf '%f\n' | sort -nr | head -n 1 -} - -get_title() { - get_title_from_file "$1/index.adoc" -} - -get_title_from_file() { - head -n 1 "$1" | sed -e 's/^[#=] //' -} - -main() { - case "$1" in - create | new | c | n) - shift 1 - cmd_create "$@" - ;; - - edit | e) - shift 1 - cmd_edit "$@" - ;; - - help | h) - show_usage - ;; - - id) - shift 1 - cmd_id "$@" - ;; - - latest) - ZID="$(get_latest_zettel)" - edit_zet "$ZID" - ;; - - links | l) - shift 1 - cmd_links "$@" - ;; - - view | v) - shift 1 - cmd_view "$@" - ;; - - *) - cmd_search "$@" - ;; - esac -} - -new_zid() { - EXISTING_ZETTELS=$(find . -maxdepth 1 -type d -regex './[0-9]+' | wc -l) - - echo $((EXISTING_ZETTELS + 1)) -} - -on_save() { - ZID="$1" - - if [[ -s "$ZID/index.adoc" ]]; then - TITLE=$(get_title "$ZID") - commit_zettel "$ZID" "$TITLE" - else - echo "Deleting empty zettel: $ZID" - delete_zettel "$ZID" - fi -} - -parse_zet_list() { - ZET_LIST=() - - while IFS= read -r ZID; do - TITLE=$(get_title "$ZID") - ZET_LIST+=("$ZID $TITLE") - done -} - -search_zettel() { - if [[ "$*" == "latest" ]] || [[ "$*" == "l" ]]; then - get_latest_zettel - return - fi - - QUERY="$*" - - grep_args=("--extended-regexp") - [[ "$QUERY" != "" ]] && grep_args+=("--word-regex") - - git grep -i --name-only "${grep_args[@]}" "$QUERY" | grep -o -E '[0-9]+' | sort -un -} - -select_zet() { - if [[ "${#ZET_LIST[@]}" == 0 ]]; then - echo "No zettels to select" - exit 1 - fi - - if [[ "${#ZET_LIST[@]}" == 1 ]]; then - SELECTED_ZET=$(awk '{ print $1 }' <<<"${ZET_LIST[0]}") - return - fi - - selector - - if [[ -z "$SELECTED_ZET" ]]; then - echo "No zet selected" - exit 1 - fi -} - -selector() { - ITEM=$(printf "%s\n" "${ZET_LIST[@]}" | fzf --prompt="Select a zet: ") - - SELECTED_ZET=$(awk '{ print $1 }' <<< "$ITEM") -} - -show_usage() { - echo "$USAGE" -} - -view_zettel() { - cat "$1/index.adoc" -} - -(cd "$ZET_DIR" && main "$@")