diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9465ef0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# synmake +config.yaml + +# Go +coverage* diff --git a/CHANGELOG/v0.1.0.md b/CHANGELOG/v0.1.0.md new file mode 100644 index 0000000..39542f7 --- /dev/null +++ b/CHANGELOG/v0.1.0.md @@ -0,0 +1,8 @@ +# v0.1.0 +## Features +- Note taking with Vim and Markdown +- Available Commands: create workspace, create node, create md, delete workspace, delete node, delete md, edit, open, version, help, ls, ls ws, goto, goback +- State is stored in metadata file in your home directory as .notewolfy +## Bug Fixes +## Notes +- When the user presses any of the arrow keys, notewolfy quits, this will be fixed in later versions \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..02b2734 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright [2024] [RaphSku] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cf5be53 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +.PHONY: default +default: help + +.PHONY: start +## Run the CLI tool +start: + @go run main.go + +.PHONY: unit-tests +## Run Unit Tests +unit-tests: + go test -coverprofile=coverage_unit.out -cover ./... -tags unit_test + go tool cover -html coverage_unit.out -o coverage_unit.html + +.PHONY: integration-tests +## Run Integration Tests +integration-tests: + go test -coverprofile=coverage_int.out -cover ./... -tags integration_test + go tool cover -html coverage_int.out -o coverage_int.html + +.PHONY: e2e-tests +## Run E2E Tests +e2e-tests: + go test -coverprofile=coverage_e2e.out -cover ./... -tags e2e_test + go tool cover -html coverage_e2e.out -o coverage_e2e.html + +.PHONY: run-test-suite +## Run Complete Test Suite +run-test-suite: unit-tests integration-tests e2e-tests + +help: + @echo "----------------------------------" + @echo "Welcome to make! Enjoy the flight." + @echo "Makefile - make [\033[38;5;154mtarget\033[0m]" + @echo "----------------------------------" + @echo + @echo "Targets:" + @awk '/^[a-zA-z\-_0-9%:\\]+/ { \ + description = match(descriptionLine, /^## (.*)/); \ + if (description) { \ + target = $$1; \ + description = substr(descriptionLine, RSTART + 3, RLENGTH); \ + gsub("\\\\", "", target); \ + gsub(":+$$", "", target); \ + printf " \033[38;5;154m%-25s\033[0m %s\n", target, description; \ + } \ + } \ + { descriptionLine = $$0 }' $(MAKEFILE_LIST) + @printf "\n" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8c94f7 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# notewolfy +## Description +notewolfy simplifies note-taking and organization into a straightforward tree-like structure. Currently, notewolfy supports note-taking through Markdown files via Vim. If you're unfamiliar with Vim, I recommend learning it before using notewolfy. Take notes of something noteworthy. + +## How to get it +You can get notewolfy with the following command: +```bash +go install github.com/RaphSku/notewolfy@latest +``` + +## How to use it +notewolfy is a console application, so open your terminal and type in: +```bash +notewolfy +``` +You will see a prompt where you can start typing: +```bash +>>> create workspace example ~/example +``` +This will create a new workspace for you named `example` at the path `~/example`. Before we start taking notes, consider organizing your workspace further. For instance, if you're researching something, you might want to create a `research` node within your workspace. +```bash +>>> create node research +``` +Now, move to your new node with +```bash +>>> goto research +``` +And let us create a Markdown file. +```bash +>>> create md research_topic_a +``` +Edit your new Markdown file and write something into it. +```bash +>>> edit research_topic_a +``` +You will see that Vim opens and will let you edit your Markdown file. Furthermore, you have seen how to navigate forward but how do we get back? Well, just go back. +```bash +>>> goback +``` +By the way, at any time you can use +```bash +>>> ls +``` +to see information about the current node you are on and which Markdown files reside there and you can use +```bash +>>> ls ws +``` +to see information about all your workspaces, namely the name and the path where they reside. This is useful if you forget the path to your workspace. +If one workspace does not suffice, just create another workspace +```bash +>>> create workspace example2 ~/example2 +``` +You will not need to switch to the new workspace since this is done automatically. But if you want to open a particular workspace, you can simply use +```bash +>>> open example2 +``` +Once you are finished, you can close notewolfy by pressing either keys: "Esc", "Ctrl+C" or type in the following commands: "quit" or "exit". + +If you want to delete Markdown files, you can do this with +```bash +>>> delete md +``` +, leave the extension .md out when you specify the name. You can only delete Markdown files on the node that you are currently on. You can also delete a node with +```bash +>>> delete node +``` +but you need to go to the parent node to delete the child node. If you have deleted all Markdown files and nodes, you can also delete the workspace. +```bash +>>> delete workspace +``` +A bulk delete is currently not supported but if you want to delete the whole workspace without going over every node and Markdown file, you can simply delete it via the file explorer or terminal. You also need to remove the workspace metadata in the `.notewolfy` metadata file that was created in your home directory. It is JSON encoded, so just remove the workspace entry under workspaces. + +If you need help with a command, try to use +```bash +help create workspace +``` + +If you want to see the version of notewolfy that you are using, just use the following command +```bash +notewolfy version +``` +or inside of notewolfy +```bash +>>> version +``` +there is also the version command available. + +## Supported OS +Only UNIX operating systems are supported. Sorry, no Windows for the time being. diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..a5c24f6 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "context" + "os" + + "github.com/RaphSku/notewolfy/cmd/version" + "github.com/RaphSku/notewolfy/internal/console" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +type CLI struct { + ctx context.Context + logger *zap.Logger + + rootCmd *cobra.Command +} + +func NewCLI(ctx context.Context, logger *zap.Logger) *CLI { + rootCmd := &cobra.Command{ + Use: "notewolfy", + Short: "notewolfy a minimalistic note taking console application", + Long: `notewolfy is a minimalistic note taking console application that allows you to organize and manage your markdown notes`, + Run: func(cmd *cobra.Command, args []string) { + console.StartConsoleApplication(ctx) + }, + } + + rootCmd.CompletionOptions.DisableDefaultCmd = true + + return &CLI{ + ctx: ctx, + logger: logger, + rootCmd: rootCmd, + } +} + +func (cli *CLI) AddSubCommands() { + versionCmd := version.NewVersionCommand() + cli.rootCmd.AddCommand(versionCmd) +} + +func (cli *CLI) Execute() { + if err := cli.rootCmd.Execute(); err != nil { + cli.logger.Info("CLI failed to run", zap.Error(err)) + os.Exit(1) + } +} diff --git a/cmd/version/version.go b/cmd/version/version.go new file mode 100644 index 0000000..0eb883a --- /dev/null +++ b/cmd/version/version.go @@ -0,0 +1,22 @@ +package version + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +const VERSION = "v0.1.0" + +func NewVersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version of notewolfy.", + Long: `This will show you the version of notewolfy in the format: {MAJOR}-{MINOR}-{PATCH}.`, + Run: versionCommandFunc, + } +} + +func versionCommandFunc(cmd *cobra.Command, args []string) { + fmt.Println(VERSION) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1e9aa0f --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/RaphSku/notewolfy + +go 1.22.5 + +require ( + github.com/google/uuid v1.6.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/RaphSku/cyclecmd v0.1.0 + github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.9.0 + go.uber.org/multierr v1.10.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7df7b41 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +github.com/RaphSku/cyclecmd v0.1.0 h1:MpYQtDMAJHId7D+q9uAo+3bq0z+8nKsfjFdw7gk8tJg= +github.com/RaphSku/cyclecmd v0.1.0/go.mod h1:UN/EsbA3Am/uBxGO9AQzhxuOI7OELTK7bBqy5GML2iE= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +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= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/commands/command.go b/internal/commands/command.go new file mode 100644 index 0000000..cb9c555 --- /dev/null +++ b/internal/commands/command.go @@ -0,0 +1,49 @@ +package commands + +import ( + "fmt" + "regexp" + "strings" + + "github.com/RaphSku/notewolfy/internal/structure" +) + +type Strategy interface { + Run() error +} + +type Context struct { + strategy Strategy +} + +func NewContext(strategy Strategy) *Context { + return &Context{strategy} +} + +func (c *Context) RunStrategy() error { + return c.strategy.Run() +} + +func validateAndTrimStatement(statement string) string { + if len(statement) == 0 { + return "" + } + re := regexp.MustCompile(`\s+`) + trimmedStatement := re.ReplaceAllString(statement, " ") + trimmedStatement = strings.TrimSpace(trimmedStatement) + + return trimmedStatement +} + +func MatchStatementToCommand(mmf *structure.MetadataNoteWolfyFileHandle, statement string) { + validatedStatement := validateAndTrimStatement(statement) + + strategy := matchStatementToStrategy(mmf, validatedStatement) + if strategy == nil { + return + } + context := NewContext(strategy) + if err := context.RunStrategy(); err != nil { + fmt.Println(err) + } +} diff --git a/internal/commands/command_test.go b/internal/commands/command_test.go new file mode 100644 index 0000000..6f09a46 --- /dev/null +++ b/internal/commands/command_test.go @@ -0,0 +1,1111 @@ +//go:build unit_test + +package commands_test + +import ( + "bytes" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/RaphSku/notewolfy/internal/commands" + "github.com/RaphSku/notewolfy/internal/structure" + "github.com/RaphSku/notewolfy/internal/utility" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func fileOrDirectoryExists(path string) (bool, error) { + expandedPath, err := utility.ExpandRelativePaths(path) + if err != nil { + return false, err + } + _, err = os.Stat(expandedPath) + if err != nil { + return false, err + } + return true, nil +} + +func createUniquePath(path string) string { + uuid := uuid.New().String() + + return fmt.Sprintf("%s-%s", path, uuid) +} + +func CleanUpFile(filePath string) { + err := os.Remove(filePath) + if err != nil { + fmt.Println("Could not remove metadata file! Please clean it up yourself!") + os.Exit(1) + } +} + +func captureStdOutput(f func()) (string, error) { + originalStdOut := os.Stdout + r, w, err := os.Pipe() + if err != nil { + return "", err + } + os.Stdout = w + + outputC := make(chan string) + go func() { + var buf bytes.Buffer + io.Copy(&buf, r) + outputC <- buf.String() + }() + + f() + w.Close() + + os.Stdout = originalStdOut + out := <-outputC + + return out, nil +} + +func TestMatchStatementToCreateWorkspaceCommand(t *testing.T) { + t.Parallel() + + firstTestCasePath := createUniquePath("./tmp") + secondTestCasePath := createUniquePath("~/tmp") + thirdTestCasePath := createUniquePath("./tmp") + fourthTestCasePath := createUniquePath("./tmp") + + tests := map[string]struct { + statement string + path string + want bool + }{ + "simple create workspace command with relative path": {statement: fmt.Sprintf("create workspace %s %s", "test", firstTestCasePath), path: firstTestCasePath, want: true}, + "simple create workspace command with absolute path": {statement: fmt.Sprintf("create workspace %s %s", "test", secondTestCasePath), path: secondTestCasePath, want: true}, + "error create workspace command": {statement: fmt.Sprintf("dgkhs create workspace %s %s", "test", thirdTestCasePath), path: thirdTestCasePath, want: false}, + "error create workspace command that almost matches": {statement: fmt.Sprintf("create workspaces %s %s", "test", fourthTestCasePath), path: fourthTestCasePath, want: false}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + statement := tc.statement + commands.MatchStatementToCommand(mmf, statement) + exists, err := fileOrDirectoryExists(tc.path) + if tc.want { + expandedPath, err := utility.ExpandRelativePaths(tc.path) + assert.NoError(t, err) + defer os.Remove(expandedPath) + + assert.NoError(t, err) + assert.True(t, exists) + + return + } + if assert.Error(t, err) { + if _, ok := err.(*fs.PathError); ok { + assert.True(t, ok) + } + } + assert.False(t, exists) + }) + } +} + +func TestMatchStatementToDeleteWorkspaceCommand(t *testing.T) { + t.Parallel() + + firstTestCasePath := createUniquePath("./tmp") + secondTestCasePath := createUniquePath("~/tmp") + thirdTestCasePath := createUniquePath("./tmp") + fourthTestCasePath := createUniquePath("./tmp") + + tests := map[string]struct { + statement string + path string + want bool + }{ + "simple delete workspace command with relative path": { + statement: fmt.Sprintf("delete workspace %s %s", "test", firstTestCasePath), + path: firstTestCasePath, + want: true, + }, + "simple delete workspace command with absolute path": { + statement: fmt.Sprintf("delete workspace %s %s", "test", secondTestCasePath), + path: secondTestCasePath, + want: true, + }, + "error delete workspace command": { + statement: fmt.Sprintf("dgkhs delete workspace %s %s", "test", thirdTestCasePath), + path: thirdTestCasePath, + want: false, + }, + "error delete workspace command that almost matches": { + statement: fmt.Sprintf("delete workspaces %s %s", "test", fourthTestCasePath), + path: fourthTestCasePath, + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // Need to prepare the workspace + workspacePath, err := utility.ExpandRelativePaths(tc.path) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "test", + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + mmf.ActiveNode = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspaceNode.Path) + + commands.MatchStatementToCommand(mmf, tc.statement) + _, err = fileOrDirectoryExists(tc.path) + if tc.want { + if assert.Error(t, err) { + if _, ok := err.(*fs.PathError); ok { + assert.True(t, ok) + } + } + assert.Empty(t, mmf.ActiveNode) + assert.Empty(t, mmf.ActiveWorkspace) + assert.Equal(t, 0, len(mmf.Workspaces)) + + return + } + assert.DirExists(t, tc.path) + }) + } +} + +func TestMatchStatementToCreateNodeCommand(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + statement string + workspacePath string + nodeName string + want bool + }{ + "simple create node command": { + statement: "create node A", + workspacePath: createUniquePath("./tmp"), + nodeName: "A", + want: true, + }, + "error create node command": { + statement: "dgkhs create node test", + workspacePath: createUniquePath("./tmp"), + nodeName: "test", + want: false, + }, + "error create node command that almost matches": { + statement: "create nodes test", + workspacePath: createUniquePath("./tmp"), + nodeName: "test", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace before we can create a node + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "Workspace", + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + mmf.ActiveNode = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + statement := tc.statement + commands.MatchStatementToCommand(mmf, statement) + nodePath := filepath.Join(workspacePath, tc.nodeName) + exists, err := fileOrDirectoryExists(nodePath) + if tc.want { + os.Remove(nodePath) + os.Remove(workspacePath) + + assert.NoError(t, err) + assert.True(t, exists) + + return + } + os.Remove(workspacePath) + if assert.Error(t, err) { + if _, ok := err.(*fs.PathError); ok { + assert.True(t, ok) + } + } + assert.False(t, exists) + }) + } +} + +func TestMatchStatementToDeleteNodeCommand(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + statement string + workspacePath string + nodeName string + want bool + }{ + "simple delete node command": { + statement: "delete node A", + workspacePath: createUniquePath("./tmp"), + nodeName: "A", + want: true, + }, + "error delete node command": { + statement: "dgkhs delete node A", + workspacePath: createUniquePath("./tmp"), + nodeName: "A", + want: false, + }, + "error delete node command that almost matches": { + statement: "delete nodes A", + workspacePath: createUniquePath("./tmp"), + nodeName: "A", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace before we can create & delete a node + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "Workspace", + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + mmf.ActiveNode = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspacePath) + + // Let's create a node that can be deleted + nodePath := filepath.Join(workspacePath, tc.nodeName) + err = os.Mkdir(nodePath, os.ModePerm) + assert.NoError(t, err) + node := &structure.Node{ + Name: tc.nodeName, + Path: nodePath, + } + mmf.Workspaces[0].Children = append(mmf.Workspaces[0].Children, node) + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(nodePath) + + statement := tc.statement + commands.MatchStatementToCommand(mmf, statement) + _, err = fileOrDirectoryExists(nodePath) + if tc.want { + if assert.Error(t, err) { + if _, ok := err.(*fs.PathError); ok { + assert.True(t, ok) + } + } + assert.Equal(t, 0, len(mmf.Workspaces[0].Children)) + return + } + assert.DirExists(t, nodePath) + }) + } +} + +func TestMatchStatementToCreateMarkdownFile(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + statement string + workspacePath string + markdownName string + want bool + }{ + "simple create markdown command": { + statement: "create md example", + workspacePath: createUniquePath("./tmp"), + markdownName: "example", + want: true, + }, + "error create markdown command": { + statement: "dgkhs create markdown example some", + workspacePath: createUniquePath("./tmp"), + markdownName: "example", + want: false, + }, + "error create markdown command that almost matches": { + statement: "create markdowns example", + workspacePath: createUniquePath("./tmp"), + markdownName: "example", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "Workspace", + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + mmf.ActiveNode = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + statement := tc.statement + commands.MatchStatementToCommand(mmf, statement) + markdownPath := filepath.Join(workspacePath, tc.markdownName+".md") + exists, err := fileOrDirectoryExists(markdownPath) + if tc.want { + os.Remove(markdownPath) + os.Remove(workspacePath) + + assert.NoError(t, err) + assert.True(t, exists) + + return + } + os.Remove(workspacePath) + if assert.Error(t, err) { + if _, ok := err.(*fs.PathError); ok { + assert.True(t, ok) + } + } + assert.False(t, exists) + }) + } +} + +func TestMatchStatementToDeleteMarkdownFile(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + statement string + workspacePath string + markdownName string + want bool + }{ + "simple delete markdown command": { + statement: "delete md example", + workspacePath: createUniquePath("./tmp"), + markdownName: "example", + want: true, + }, + "error delete markdown command": { + statement: "dgkhs delete md example some", + workspacePath: createUniquePath("./tmp"), + markdownName: "example", + want: false, + }, + "error delete markdown command that almost matches": { + statement: "delete markdown example", + workspacePath: createUniquePath("./tmp"), + markdownName: "example", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "Workspace", + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + mmf.ActiveNode = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspacePath) + + // We need to create a Markdown file that we can delete + markdown := &structure.Markdown{ + Filename: tc.markdownName + ".md", + } + markdownPath := filepath.Join(workspacePath, tc.markdownName+".md") + file, err := os.Create(markdownPath) + assert.NoError(t, err) + defer os.Remove(markdownPath) + defer file.Close() + mmf.Workspaces[0].Markdowns = append(mmf.Workspaces[0].Markdowns, markdown) + err = mmf.Save() + assert.NoError(t, err) + + statement := tc.statement + commands.MatchStatementToCommand(mmf, statement) + exists, err := fileOrDirectoryExists(markdownPath) + if tc.want { + if assert.Error(t, err) { + if _, ok := err.(*fs.PathError); ok { + assert.True(t, ok) + } + } + assert.False(t, exists) + + return + } + assert.NoError(t, err) + assert.True(t, exists) + }) + } +} + +func TestMatchStatementToGotoNode(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + statement string + workspacePath string + nodeName string + want bool + }{ + "simple goto command": { + statement: "goto example", + workspacePath: createUniquePath("./tmp"), + nodeName: "example", + want: true, + }, + "error goto command": { + statement: "gotos example", + workspacePath: createUniquePath("./tmp"), + nodeName: "example", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "Workspace", + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + mmf.ActiveNode = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + // We need to create a node + nodePath := filepath.Join(tc.workspacePath, tc.nodeName) + err = os.Mkdir(nodePath, os.ModePerm) + assert.NoError(t, err) + node := &structure.Node{ + Name: tc.nodeName, + Path: nodePath, + } + mmf.Workspaces[0].Children = append(mmf.Workspaces[0].Children, node) + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspacePath) + defer os.Remove(nodePath) + + statement := tc.statement + commands.MatchStatementToCommand(mmf, statement) + if tc.want { + assert.Equal(t, node.Name, mmf.ActiveNode) + actNode := mmf.FindNode(node.Name) + assert.True(t, reflect.DeepEqual(node, actNode)) + + return + } + assert.Equal(t, workspaceNode.Name, mmf.ActiveNode) + }) + } +} + +func TestMatchStatementToGoBack(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + statement string + workspacePath string + nodeName string + want bool + }{ + "simple goback command": { + statement: "goback", + workspacePath: createUniquePath("./tmp"), + nodeName: "example", + want: true, + }, + "error goback command": { + statement: "gobacks", + workspacePath: createUniquePath("./tmp"), + nodeName: "example", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "Workspace", + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + // We need to create a node + nodePath := filepath.Join(tc.workspacePath, tc.nodeName) + err = os.Mkdir(nodePath, os.ModePerm) + assert.NoError(t, err) + node := &structure.Node{ + Name: tc.nodeName, + Path: nodePath, + } + mmf.ActiveNode = node.Name + mmf.Workspaces[0].Children = append(mmf.Workspaces[0].Children, node) + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspacePath) + defer os.Remove(nodePath) + + statement := tc.statement + commands.MatchStatementToCommand(mmf, statement) + if tc.want { + assert.Equal(t, workspaceNode.Name, mmf.ActiveNode) + + return + } + assert.Equal(t, node.Name, mmf.ActiveNode) + }) + } +} + +func TestMatchStatementToOpen(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + statement string + workspacePath string + workspaceName string + nodeName string + want bool + }{ + "simple open command": { + statement: "open test", + workspacePath: createUniquePath("./tmp"), + workspaceName: "test", + nodeName: "example", + want: true, + }, + "error open command": { + statement: "opens test", + workspacePath: createUniquePath("./tmp"), + workspaceName: "test", + nodeName: "example", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: tc.workspaceName, + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + // We need to create a node + nodePath := filepath.Join(tc.workspacePath, tc.nodeName) + err = os.Mkdir(nodePath, os.ModePerm) + assert.NoError(t, err) + node := &structure.Node{ + Name: tc.nodeName, + Path: nodePath, + } + mmf.ActiveNode = node.Name + mmf.Workspaces[0].Children = append(mmf.Workspaces[0].Children, node) + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspacePath) + defer os.Remove(nodePath) + + statement := tc.statement + commands.MatchStatementToCommand(mmf, statement) + if tc.want { + assert.Equal(t, workspaceNode.Name, mmf.ActiveWorkspace) + assert.Equal(t, workspaceNode.Name, mmf.ActiveNode) + + return + } + assert.Equal(t, node.Name, mmf.ActiveNode) + }) + } +} + +func TestMatchStatementToEdit(t *testing.T) { + tests := map[string]struct { + statement string + workspacePath string + want bool + }{ + "simple edit command": { + statement: "edit example", + workspacePath: createUniquePath("./tmp"), + want: true, + }, + "error edit command": { + statement: "edits example", + workspacePath: createUniquePath("./tmp"), + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "workspace", + Path: workspacePath, + } + mmf.ActiveNode = workspaceNode.Name + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveWorkspace = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + // We need to create a markdown file + markdownFileName := "example.md" + markdownFilePath := filepath.Join(workspacePath, markdownFileName) + markdown := &structure.Markdown{ + Filename: markdownFileName, + } + file, err := os.Create(markdownFilePath) + assert.NoError(t, err) + mmf.Workspaces[0].Markdowns = append(mmf.Workspaces[0].Markdowns, markdown) + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspacePath) + defer func() { + file.Close() + os.Remove(file.Name()) + }() + + tempFile, err := os.CreateTemp("", "tempStdin") + assert.NoError(t, err) + defer os.Remove(tempFile.Name()) + + _, err = tempFile.WriteString("iHelloWorld!\nThis is a Test!\x1b:wq\n") + assert.NoError(t, err) + err = tempFile.Close() + assert.NoError(t, err) + + tempFile, err = os.Open(tempFile.Name()) + assert.NoError(t, err) + defer tempFile.Close() + + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + os.Stdin = tempFile + + statement := tc.statement + _, err = captureStdOutput(func() { + commands.MatchStatementToCommand(mmf, statement) + }) + assert.NoError(t, err) + if tc.want { + content, err := os.ReadFile(markdownFilePath) + assert.NoError(t, err) + actContentString := string(content) + expContentString := "HelloWorld!\nThis is a Test!\n" + assert.Equal(t, expContentString, actContentString) + + return + } + }) + } +} + +func TestMatchStatementToVersion(t *testing.T) { + tests := map[string]struct { + statement string + want bool + }{ + "simple version command": { + statement: "version", + want: true, + }, + "error version command": { + statement: "versions", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + actOutput, err := captureStdOutput(func() { + commands.MatchStatementToCommand(mmf, tc.statement) + }) + assert.NoError(t, err) + if tc.want { + expContentString := "\n\rnotewolfy version v0.1.0 at your disposal!" + assert.Equal(t, expContentString, actOutput) + + return + } + }) + } +} + +func TestMatchStatementToList(t *testing.T) { + tests := map[string]struct { + statement string + workspacePath string + workspaceName string + want bool + }{ + "simple list command": { + statement: "ls", + workspacePath: createUniquePath("./tmp"), + workspaceName: "test", + want: true, + }, + "error list command": { + statement: "lss", + workspacePath: createUniquePath("./tmp"), + workspaceName: "test", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspace + workspacePath, err := utility.ExpandRelativePaths(tc.workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: tc.workspaceName, + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveNode = workspaceNode.Name + mmf.ActiveWorkspace = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + // We need to create nodes and markdown files that ls can display + nodePathA := filepath.Join(tc.workspacePath, "A") + nodeA := &structure.Node{ + Name: "A", + Path: nodePathA, + } + nodePathB := filepath.Join(tc.workspacePath, "B") + nodeB := &structure.Node{ + Name: "B", + Path: nodePathB, + } + markdown := &structure.Markdown{ + Filename: "example.md", + } + mmf.Workspaces[0].Children = append(mmf.Workspaces[0].Children, nodeA) + mmf.Workspaces[0].Children = append(mmf.Workspaces[0].Children, nodeB) + mmf.Workspaces[0].Markdowns = append(mmf.Workspaces[0].Markdowns, markdown) + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspacePath) + + statement := tc.statement + actOutput, err := captureStdOutput(func() { + commands.MatchStatementToCommand(mmf, statement) + }) + assert.NoError(t, err) + if tc.want { + expOutput := "\r\nYou are on node: test\n\rChild nodes:\n\r A\n\r B\n\rMarkdown files:\n\r example.md\n" + assert.Equal(t, expOutput, actOutput) + + return + } + assert.Empty(t, actOutput) + }) + } +} + +func TestMatchStatementToListWorkspaces(t *testing.T) { + // Note: These tests only work with ./ as a base path + tests := map[string]struct { + statement string + workspacePaths []string + workspaceName string + want bool + }{ + "simple ls ws command": { + statement: "ls ws", + workspacePaths: []string{createUniquePath("./tmpA"), createUniquePath("./tmpB")}, + workspaceName: "test", + want: true, + }, + "error ls ws command": { + statement: "ls s ws", + workspacePaths: []string{createUniquePath("./tmpA"), createUniquePath("./tmpB")}, + workspaceName: "test", + want: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + // We need to prepare the workspaces + workspacePathA, err := utility.ExpandRelativePaths(tc.workspacePaths[0]) + assert.NoError(t, err) + workspaceNodeA := &structure.Node{ + Name: tc.workspaceName + "A", + Path: workspacePathA, + } + workspacePathB, err := utility.ExpandRelativePaths(tc.workspacePaths[1]) + assert.NoError(t, err) + workspaceNodeB := &structure.Node{ + Name: tc.workspaceName + "B", + Path: workspacePathB, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNodeA) + mmf.Workspaces = append(mmf.Workspaces, workspaceNodeB) + mmf.ActiveNode = workspaceNodeA.Name + mmf.ActiveWorkspace = workspaceNodeA.Name + err = mmf.Save() + assert.NoError(t, err) + + defer os.Remove(workspacePathA) + defer os.Remove(workspacePathB) + + statement := tc.statement + actOutput, err := captureStdOutput(func() { + commands.MatchStatementToCommand(mmf, statement) + }) + assert.NoError(t, err) + if tc.want { + basePath, err := utility.ExpandRelativePaths("./") + assert.NoError(t, err) + workspaceNameA := tc.workspacePaths[0][2:] + workspaceNameB := tc.workspacePaths[1][2:] + expOutput := fmt.Sprintf("\r\nWorkspace Name Workspace Path \r\n---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n\rtestA %[1]s/%[2]s\n\rtestB %[1]s/%[3]s\n", basePath, workspaceNameA, workspaceNameB) + assert.Equal(t, expOutput, actOutput) + + return + } + expOutput := "\r\nYou are on node: testA\n\rChild nodes:\n\rMarkdown files:\n" + assert.Equal(t, expOutput, actOutput) + }) + } +} + +func TestMatchStatementToHelp(t *testing.T) { + tests := map[string]struct { + statement string + expOutput string + }{ + "simple help command (1)": { + statement: "help ls", + expOutput: "\n\rCommand: ls\n\rDescription: ls can be used to list information about the node that you are on, e.g. active node, markdown files on that node, etc.\n\rExample Usage: ls", + }, + "simple help command (2)": { + statement: "help create workspace", + expOutput: "\n\rCommand: create workspace \n\rDescription: create workspace will create a new workspace for you under the specified name and path that you can choose.\n\rExample Usage: create workspace example /path/to/example", + }, + "error help command": { + statement: "help something", + expOutput: "", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + actOutput, err := captureStdOutput(func() { + commands.MatchStatementToCommand(mmf, tc.statement) + }) + assert.NoError(t, err) + assert.Equal(t, tc.expOutput, actOutput) + }) + } +} diff --git a/internal/commands/goback.go b/internal/commands/goback.go new file mode 100644 index 0000000..3111837 --- /dev/null +++ b/internal/commands/goback.go @@ -0,0 +1,20 @@ +package commands + +import ( + "github.com/RaphSku/notewolfy/internal/structure" +) + +type GoBackStrategy struct { + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (gbs *GoBackStrategy) Run() error { + activeNodeName := gbs.mmf.ActiveNode + parentNode := gbs.mmf.FindParentNode(activeNodeName) + if parentNode != nil { + gbs.mmf.ActiveNode = parentNode.Name + gbs.mmf.Save() + } + + return nil +} diff --git a/internal/commands/goto.go b/internal/commands/goto.go new file mode 100644 index 0000000..bf36ae6 --- /dev/null +++ b/internal/commands/goto.go @@ -0,0 +1,40 @@ +package commands + +import ( + "fmt" + "regexp" + + "github.com/RaphSku/notewolfy/internal/structure" +) + +type GoToStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (gts *GoToStrategy) Run() error { + goToRegex := regexp.MustCompile("goto (?P[[:alpha:]]+)") + matches := goToRegex.FindStringSubmatch(gts.statement) + names := goToRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + goToName := namedGroups["name"] + + activeNodeName := gts.mmf.ActiveNode + activeNode := gts.mmf.FindNode(activeNodeName) + for _, childNode := range activeNode.Children { + if childNode.Name == goToName { + gts.mmf.ActiveNode = childNode.Name + gts.mmf.Save() + return nil + } + } + + fmt.Println("\r\nCould not find node ", goToName) + + return nil +} diff --git a/internal/commands/help.go b/internal/commands/help.go new file mode 100644 index 0000000..b8844b0 --- /dev/null +++ b/internal/commands/help.go @@ -0,0 +1,80 @@ +package commands + +import ( + "fmt" + "regexp" +) + +type HelpStrategy struct { + statement string +} + +func (hs *HelpStrategy) Run() error { + helpRegex := regexp.MustCompile("^help (?P[[:alpha:]]+(?: [[:alpha:]]+)*)") + matches := helpRegex.FindStringSubmatch(hs.statement) + names := helpRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + helpCommand := namedGroups["name"] + + var command string + var description string + var example string + switch helpCommand { + case "ls": + command = "\n\rCommand: ls" + description = "\n\rDescription: ls can be used to list information about the node that you are on, e.g. active node, markdown files on that node, etc." + example = "\n\rExample Usage: ls" + case "ls ws": + command = "\n\rCommand: ls ws" + description = "\n\rDescription: ls ws will list the workspaces and their root paths in a table format." + example = "\n\rExample Usage: ls ws" + case "create workspace": + command = "\n\rCommand: create workspace " + description = "\n\rDescription: create workspace will create a new workspace for you under the specified name and path that you can choose." + example = "\n\rExample Usage: create workspace example /path/to/example" + case "delete workspace": + command = "\n\rCommand: delete workspace " + description = "\n\rDescription: delete workspace lets you delete the specified workspace. This will fail if nodes & markdown files still exist on the node." + example = "\n\rExample Usage: delete workspace example" + case "create node": + command = "\n\rCommand: create node " + description = "\n\rDescription: create node will create a new node for you under the specified name. The node path will correspond to /pathOfActiveNode/nodeName." + example = "\n\rExample Usage: create node example" + case "delete node": + command = "\n\rCommand: delete node " + description = "\n\rDescription: delete node lets you delete the specified node. This will fail if markdown files still exist on the node." + example = "\n\rExample Usage: delete node example" + case "create md": + command = "\n\rCommand: create md " + description = "\n\rDescription: create md will create a new markdown file for you under the specified name. You don't need to append the file extension to the name." + example = "\n\rExample Usage: create md example" + case "delete md": + command = "\n\rCommand: delete md " + description = "\n\rDescription: delete md lets you delete the specified markdown file. Specify only the name, so without the file extension." + example = "\n\rExample Usage: delete md example" + case "goto": + command = "\n\rCommand: goto " + description = "\n\rDescription: goto will let you change the node, specify the name of the node that is a direct child of the node that you are on." + example = "\n\rExample Usage: goto example" + case "goback": + command = "\n\rCommand: goback" + description = "\n\rDescription: goback lets you go to the parent node of the node that you are currently on." + example = "\n\rExample Usage: goback" + case "open": + command = "\n\rCommand: open " + description = "\n\rDescription: open lets you open another workspace, in the sense that the active node will be set to the specified workspace node." + example = "\n\rExample Usage: open example" + case "version": + command = "\n\rCommand: version" + description = "\n\rDescription: version will print notewolfy's version." + example = "\n\rExample Usage: version" + } + fmt.Printf(command + description + example) + + return nil +} diff --git a/internal/commands/list.go b/internal/commands/list.go new file mode 100644 index 0000000..11603aa --- /dev/null +++ b/internal/commands/list.go @@ -0,0 +1,26 @@ +package commands + +import ( + "fmt" + + "github.com/RaphSku/notewolfy/internal/structure" +) + +type ListStrategy struct { + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (ls *ListStrategy) Run() error { + activeNodeName := ls.mmf.ActiveNode + if activeNodeName == "" { + fmt.Print("\n\rSeems like you have not created a workspace yet! Create one with 'create workspace '") + return nil + } + activeNode := ls.mmf.FindNode(activeNodeName) + if activeNode == nil { + fmt.Print("\n\rSeems like you have not created a workspace yet! At least no active node is set!") + } + ls.mmf.ListResourcesOnNode(activeNode) + + return nil +} diff --git a/internal/commands/listworkspaces.go b/internal/commands/listworkspaces.go new file mode 100644 index 0000000..6d0e629 --- /dev/null +++ b/internal/commands/listworkspaces.go @@ -0,0 +1,16 @@ +package commands + +import ( + "github.com/RaphSku/notewolfy/internal/structure" +) + +type ListWorkspacesStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (lws *ListWorkspacesStrategy) Run() error { + lws.mmf.ListWorkspaces() + + return nil +} diff --git a/internal/commands/markdown.go b/internal/commands/markdown.go new file mode 100644 index 0000000..eabfa7c --- /dev/null +++ b/internal/commands/markdown.go @@ -0,0 +1,125 @@ +package commands + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/RaphSku/notewolfy/internal/structure" +) + +type CreateMarkdownStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (cms *CreateMarkdownStrategy) Run() error { + markdownNameRegex := regexp.MustCompile("create md (?P[\\w]+)") + matches := markdownNameRegex.FindStringSubmatch(cms.statement) + names := markdownNameRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + markdownName := namedGroups["name"] + markdownNameWithFExt := strings.Join([]string{markdownName, ".md"}, "") + activeNodeName := cms.mmf.ActiveNode + activeNode := cms.mmf.FindNode(activeNodeName) + pathToMarkdown := filepath.Join(activeNode.Path, markdownNameWithFExt) + + _, err := os.Stat(pathToMarkdown) + if os.IsNotExist(err) { + markdown := &structure.Markdown{ + Filename: markdownNameWithFExt, + } + cms.mmf.AddMarkdown(markdown) + cms.mmf.Save() + + file, err := os.Create(pathToMarkdown) + if err != nil { + return err + } + defer file.Close() + + return nil + } + + fmt.Println("\r\nMarkdown file already exists!") + + return nil +} + +type DeleteMDStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (dms *DeleteMDStrategy) Run() error { + markdownNameRegex := regexp.MustCompile("delete md (?P[\\w]+)") + matches := markdownNameRegex.FindStringSubmatch(dms.statement) + names := markdownNameRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + markdownName := namedGroups["name"] + + activeNodeName := dms.mmf.ActiveNode + activeNode := dms.mmf.FindNode(activeNodeName) + err := os.Remove(filepath.Join(activeNode.Path, strings.Join([]string{markdownName, ".md"}, ""))) + if err != nil { + return err + } + + err = dms.mmf.DeleteMarkdown(markdownName) + if err != nil { + return err + } + dms.mmf.Save() + + return nil +} + +type EditStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (es *EditStrategy) Run() error { + markdownNameRegex := regexp.MustCompile("edit (?P[\\w]+)") + matches := markdownNameRegex.FindStringSubmatch(es.statement) + names := markdownNameRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + markdownName := namedGroups["name"] + activeNodeName := es.mmf.ActiveNode + activeNode := es.mmf.FindNode(activeNodeName) + for _, markdown := range activeNode.Markdowns { + if markdown.Filename[:len(markdown.Filename)-3] == markdownName { + markdownFile := filepath.Join(activeNode.Path, markdown.Filename) + + cmd := exec.Command("vi", markdownFile) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return err + } + } + } + + return nil +} diff --git a/internal/commands/node.go b/internal/commands/node.go new file mode 100644 index 0000000..eeb8285 --- /dev/null +++ b/internal/commands/node.go @@ -0,0 +1,102 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + + "github.com/RaphSku/notewolfy/internal/structure" + "github.com/RaphSku/notewolfy/internal/utility" +) + +type CreateNodeStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (cns *CreateNodeStrategy) Run() error { + nodeNameRegex := regexp.MustCompile("create node (?P[\\w]+)") + matches := nodeNameRegex.FindStringSubmatch(cns.statement) + names := nodeNameRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + nodeName := namedGroups["name"] + + activeNodeName := cns.mmf.ActiveNode + activeNode := cns.mmf.FindNode(activeNodeName) + pathToNode, err := utility.ExpandRelativePaths(filepath.Join(activeNode.Path, nodeName)) + if err != nil { + return err + } + + var children []*structure.Node + var markdowns []*structure.Markdown + childNode := &structure.Node{ + Name: nodeName, + Path: pathToNode, + Markdowns: markdowns, + Children: children, + } + err = cns.mmf.AddChild(childNode) + if err != nil { + return err + } + cns.mmf.Save() + + err = os.Mkdir(pathToNode, 0750) + if err != nil { + return err + } + + return nil +} + +type DeleteNodeStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (dns *DeleteNodeStrategy) Run() error { + nodeNameRegex := regexp.MustCompile("delete node (?P[\\w]+)") + matches := nodeNameRegex.FindStringSubmatch(dns.statement) + names := nodeNameRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + nodeName := namedGroups["name"] + + activeNodeName := dns.mmf.ActiveNode + activeNode := dns.mmf.FindNode(activeNodeName) + for index, child := range activeNode.Children { + if child.Name == nodeName { + if len(child.Markdowns) != 0 || len(child.Children) != 0 { + return fmt.Errorf("please delete all subsequent nodes and markdown files before deleting %s", nodeName) + } + + err := dns.mmf.DeleteChildByIndex(index) + if err != nil { + return err + } + err = dns.mmf.Save() + if err != nil { + return err + } + + err = os.Remove(child.Path) + if err != nil { + return err + } + return nil + } + } + + return fmt.Errorf("there is no node with the name %s", nodeName) +} diff --git a/internal/commands/open.go b/internal/commands/open.go new file mode 100644 index 0000000..54496b7 --- /dev/null +++ b/internal/commands/open.go @@ -0,0 +1,35 @@ +package commands + +import ( + "regexp" + + "github.com/RaphSku/notewolfy/internal/structure" +) + +type OpenStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (ops *OpenStrategy) Run() error { + workspaceNameRegex := regexp.MustCompile("open (?P[[:alpha:]]+)") + matches := workspaceNameRegex.FindStringSubmatch(ops.statement) + names := workspaceNameRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + workspaceName := namedGroups["name"] + + for _, workspace := range ops.mmf.Workspaces { + if workspace.Name == workspaceName { + ops.mmf.ActiveWorkspace = workspace.Name + ops.mmf.ActiveNode = workspace.Name + ops.mmf.Save() + } + } + + return nil +} diff --git a/internal/commands/strategies.go b/internal/commands/strategies.go new file mode 100644 index 0000000..3dec4a8 --- /dev/null +++ b/internal/commands/strategies.go @@ -0,0 +1,88 @@ +package commands + +import ( + "strings" + + "github.com/RaphSku/notewolfy/internal/structure" +) + +func matchStatementToStrategy(mmf *structure.MetadataNoteWolfyFileHandle, statement string) Strategy { + strategies := map[string]Strategy{ + "version": &VersionStrategy{}, + "create workspace": &CreateWorkspaceStrategy{ + statement: statement, + mmf: mmf, + }, + "delete workspace": &DeleteWorkspaceStrategy{ + statement: statement, + mmf: mmf, + }, + "ls ws": &ListWorkspacesStrategy{ + statement: statement, + mmf: mmf, + }, + "create node": &CreateNodeStrategy{ + statement: statement, + mmf: mmf, + }, + "delete node": &DeleteNodeStrategy{ + statement: statement, + mmf: mmf, + }, + "create md": &CreateMarkdownStrategy{ + statement: statement, + mmf: mmf, + }, + "delete md": &DeleteMDStrategy{ + statement: statement, + mmf: mmf, + }, + "open": &OpenStrategy{ + statement: statement, + mmf: mmf, + }, + "edit": &EditStrategy{ + statement: statement, + mmf: mmf, + }, + "ls": &ListStrategy{ + mmf: mmf, + }, + "goto": &GoToStrategy{ + statement: statement, + mmf: mmf, + }, + "goback": &GoBackStrategy{ + mmf: mmf, + }, + "help": &HelpStrategy{ + statement: statement, + }, + } + + var longestPrefixMatch string + longestPrefixLength := 0 + for command := range strategies { + if len(command) <= len(statement) { + matches := true + for i := range command { + if statement[i] != command[i] { + matches = false + break + } + } + if matches && len(command) > longestPrefixLength { + if len(command) == len(statement) || (len(command) < len(statement) && strings.HasPrefix(string(statement[len(command)]), " ")) { + longestPrefixMatch = command + longestPrefixLength = len(command) + } + } + } + } + + if longestPrefixLength == 0 { + return nil + } + + return strategies[longestPrefixMatch] +} diff --git a/internal/commands/version.go b/internal/commands/version.go new file mode 100644 index 0000000..eeeb8c6 --- /dev/null +++ b/internal/commands/version.go @@ -0,0 +1,14 @@ +package commands + +import ( + "fmt" + + "github.com/RaphSku/notewolfy/cmd/version" +) + +type VersionStrategy struct{} + +func (vs *VersionStrategy) Run() error { + fmt.Printf("\n\rnotewolfy version %s at your disposal!", version.VERSION) + return nil +} diff --git a/internal/commands/workspace.go b/internal/commands/workspace.go new file mode 100644 index 0000000..546fb72 --- /dev/null +++ b/internal/commands/workspace.go @@ -0,0 +1,99 @@ +package commands + +import ( + "fmt" + "os" + "regexp" + + "github.com/RaphSku/notewolfy/internal/structure" + "github.com/RaphSku/notewolfy/internal/utility" +) + +type CreateWorkspaceStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (cws *CreateWorkspaceStrategy) Run() error { + pathRegex := regexp.MustCompile("[~.]{0,1}/.*") + workspacePath := pathRegex.FindAllString(cws.statement, 1) + if len(workspacePath) == 0 { + return nil + } + + workspaceNameRegex := regexp.MustCompile("create workspace (?P[\\w]+) [~/.]{0,1}/.*") + matches := workspaceNameRegex.FindStringSubmatch(cws.statement) + names := workspaceNameRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + workspaceName := namedGroups["name"] + + pathToWorkspace, err := utility.ExpandRelativePaths(workspacePath[0]) + if err != nil { + return err + } + cws.mmf.AddNewWorkspace(workspaceName, pathToWorkspace) + cws.mmf.Save() + + err = os.Mkdir(pathToWorkspace, 0755) + if err != nil { + return err + } + + return nil +} + +type DeleteWorkspaceStrategy struct { + statement string + mmf *structure.MetadataNoteWolfyFileHandle +} + +func (dws *DeleteWorkspaceStrategy) Run() error { + workspaceNameRegex := regexp.MustCompile("delete workspace (?P[\\w]+)") + matches := workspaceNameRegex.FindStringSubmatch(dws.statement) + names := workspaceNameRegex.SubexpNames() + namedGroups := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { + namedGroups[name] = matches[i] + } + } + workspaceName := namedGroups["name"] + + foundIndex := -1 + for index, workspace := range dws.mmf.Workspaces { + if workspace.Name == workspaceName { + foundIndex = index + } + } + if foundIndex == -1 { + return fmt.Errorf("workspace %s could not be found!", workspaceName) + } + + if len(dws.mmf.Workspaces[foundIndex].Children) != 0 || len(dws.mmf.Workspaces[foundIndex].Markdowns) != 0 { + return fmt.Errorf("before you delete a workspace, ensure that you have deleted all nodes and markdown files in this workspace!") + } + + workspacePath := dws.mmf.Workspaces[foundIndex].Path + + dws.mmf.Workspaces = append(dws.mmf.Workspaces[:foundIndex], dws.mmf.Workspaces[foundIndex+1:]...) + if len(dws.mmf.Workspaces) != 0 { + dws.mmf.ActiveWorkspace = dws.mmf.Workspaces[0].Name + dws.mmf.ActiveNode = dws.mmf.Workspaces[0].Name + } else { + dws.mmf.ActiveWorkspace = "" + dws.mmf.ActiveNode = "" + } + dws.mmf.Save() + + err := os.Remove(workspacePath) + if err != nil { + return fmt.Errorf("the workspace %s could not be deleted, please clean up the following workspace path yourself: %s, error: %v", workspaceName, workspacePath, err) + } + + return nil +} diff --git a/internal/console/app.go b/internal/console/app.go new file mode 100644 index 0000000..44b6e33 --- /dev/null +++ b/internal/console/app.go @@ -0,0 +1,62 @@ +package console + +import ( + "context" + + "github.com/RaphSku/cyclecmd" + "github.com/RaphSku/notewolfy/cmd/version" +) + +var eventHistory *cyclecmd.EventHistory + +func getEventHistory() *cyclecmd.EventHistory { + if eventHistory == nil { + eventHistory = cyclecmd.NewEventHistory() + } + return eventHistory +} + +func StartConsoleApplication(ctx context.Context) { + defaultEventInformation := cyclecmd.EventInformation{ + EventName: "Default", + Event: &DefaultEvent{}, + } + eventRegistry := cyclecmd.NewEventRegistry(defaultEventInformation) + + backspaceEventInformation := cyclecmd.EventInformation{ + EventName: "Backspace", + Event: &BackspaceEvent{}, + } + eventRegistry.RegisterEvent("\x7f", backspaceEventInformation) + + enterEventInformation := cyclecmd.EventInformation{ + EventName: "Enter", + Event: &EnterEvent{}, + } + eventRegistry.RegisterEvent("\r", enterEventInformation) + + escEventInformation := cyclecmd.EventInformation{ + EventName: "Escape", + Event: &EscapeEvent{}, + } + eventRegistry.RegisterEvent("\x03", escEventInformation) + + ctrlcEventInformation := cyclecmd.EventInformation{ + EventName: "Ctrl+C", + Event: &CtrlCEvent{}, + } + eventRegistry.RegisterEvent("\x1b", ctrlcEventInformation) + + eventHistory := getEventHistory() + + consoleApp := cyclecmd.NewConsoleApp( + context.Background(), + "notewolfy", + version.VERSION, + "Creating organized notes is just easy with notewolfy", + eventRegistry, + eventHistory, + ) + consoleApp.SetLineDelimiter("\n\r>>> ", "\r") + consoleApp.Start() +} diff --git a/internal/console/events.go b/internal/console/events.go new file mode 100644 index 0000000..7460539 --- /dev/null +++ b/internal/console/events.go @@ -0,0 +1,145 @@ +package console + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/RaphSku/notewolfy/internal/commands" + "github.com/RaphSku/notewolfy/internal/structure" + "github.com/RaphSku/notewolfy/internal/utility" +) + +type DefaultEvent struct{} + +func (de *DefaultEvent) Handle(token string) error { + fmt.Print(token) + return nil +} + +type BackspaceEvent struct{} + +func (be *BackspaceEvent) Handle(token string) error { + eventHistory := getEventHistory() + var filteredEventNames []string + eventNamesFromHistory := eventHistory.GetLastEventsFromHistoryToEventReference("Enter") + for _, eventName := range eventNamesFromHistory { + if eventName != "Backspace" { + filteredEventNames = append(filteredEventNames, eventName) + } + } + if len(filteredEventNames) == 0 { + return nil + } else { + eventHistoryLength := eventHistory.Len() + // Backspace will only work if we remove the backspace event itself and the event previous to the backspace + eventHistory.RemoveNthEventFromHistory(eventHistoryLength - 1) + eventHistory.RemoveNthEventFromHistory(eventHistoryLength - 2) + } + fmt.Print("\b \b") + return nil +} + +var mmf *structure.MetadataNoteWolfyFileHandle + +func InitMetadataNoteWolfyFileHandle() error { + if mmf == nil { + homeDir, err := utility.GetHomeDir() + if err != nil { + return err + } + metadataFilePath := filepath.Join(homeDir, ".notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + newmmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + if err != nil { + return err + } + mmf = newmmf + } + return nil +} + +type EnterEvent struct{} + +func (ee *EnterEvent) Handle(token string) error { + err := InitMetadataNoteWolfyFileHandle() + if err != nil { + return err + } + statement := buildStatement() + handleEnter(mmf, statement) + return nil +} + +type EscapeEvent struct{} + +func (ee *EscapeEvent) Handle(token string) error { + if checkEscExitCondition(token) { + quitConsole() + } + return nil +} + +type CtrlCEvent struct{} + +func (ce *CtrlCEvent) Handle(token string) error { + if checkCtrlCExitCondition(token) { + quitConsole() + } + return nil +} + +func checkEscExitCondition(token string) bool { + return token == "\x03" +} + +func checkCtrlCExitCondition(token string) bool { + // TODO: token \x1b stands also for the arrow keys + return token == "\x1b" +} + +func quitConsole() { + fmt.Print("\n\rThank you for using notewolfy!") + os.Exit(0) +} + +func buildStatement() string { + eventHistory := getEventHistory() + var statement string + splicedEventEntries := eventHistory.MostRecentSpliceEventsOfHistory("Enter") + for _, eventEntry := range splicedEventEntries { + if _, ok := eventEntry.Event.(*DefaultEvent); ok { + statement += eventEntry.Token + continue + } + if _, ok := eventEntry.Event.(*BackspaceEvent); ok { + if len(statement) == 0 { + continue + } + statement = statement[:len(statement)-1] + continue + } + } + return statement +} + +func handleEnter(mmf *structure.MetadataNoteWolfyFileHandle, statement string) { + fmt.Print("\r") + if checkExitCommand(statement) { + quitConsole() + } + commands.MatchStatementToCommand(mmf, statement) +} + +func checkExitCommand(statement string) bool { + shouldBreak := false + switch statement { + case "exit": + shouldBreak = true + case "quit": + shouldBreak = true + } + return shouldBreak +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go new file mode 100644 index 0000000..4129efa --- /dev/null +++ b/internal/logging/logger.go @@ -0,0 +1,33 @@ +package logging + +import ( + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func SetupZapLogger(debug bool) *zap.Logger { + if !debug { + return zap.NewNop() + } + logLevel := zapcore.DebugLevel + + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + + core := zapcore.NewCore( + zapcore.NewConsoleEncoder(encoderConfig), + zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)), + logLevel, + ) + + loggerOptions := []zap.Option{ + zap.AddStacktrace(zapcore.ErrorLevel), + zap.Fields(zap.String("service", "synmake")), + } + + logger := zap.New(core, loggerOptions...) + + return logger +} diff --git a/internal/structure/metadata.go b/internal/structure/metadata.go new file mode 100644 index 0000000..ba2f936 --- /dev/null +++ b/internal/structure/metadata.go @@ -0,0 +1,282 @@ +package structure + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + "github.com/RaphSku/notewolfy/internal/utility" +) + +type Config struct { + MetadataFilePath string +} + +type Markdown struct { + Filename string `json:"filename"` +} + +type Node struct { + Name string `json:"name"` + Path string `json:"path"` + Markdowns []*Markdown `json:"markdowns"` + Children []*Node `json:"children"` +} + +type MetadataNoteWolfyFileHandle struct { + Config *Config `json:"-"` + Workspaces []*Node `json:"workspaces"` + ActiveWorkspace string `json:"activeworkspace"` + ActiveNode string `json:"activenode"` +} + +func NewMetadataNoteWolfyFileHandle(config *Config) (*MetadataNoteWolfyFileHandle, error) { + notewolfyFileHandle := &MetadataNoteWolfyFileHandle{Config: config} + err := notewolfyFileHandle.load() + if err != nil { + return nil, err + } + return notewolfyFileHandle, nil +} + +func (mmf *MetadataNoteWolfyFileHandle) AddNewWorkspace(workspaceName string, workspacePath string) error { + expanedWorkspacePath, err := utility.ExpandRelativePaths(workspacePath) + if err != nil { + return err + } + var nodes []*Node + var markdowns []*Markdown + newWorkspaceNode := &Node{ + Name: workspaceName, + Path: expanedWorkspacePath, + Markdowns: markdowns, + Children: nodes, + } + mmf.Workspaces = append(mmf.Workspaces, newWorkspaceNode) + mmf.ActiveWorkspace = workspaceName + mmf.ActiveNode = workspaceName + + return nil +} + +func (mmf *MetadataNoteWolfyFileHandle) DoesWorkspaceExist(name string) bool { + doesExist := false + for _, node := range mmf.Workspaces { + if node.Name == name { + doesExist = true + } + } + + return doesExist +} + +func (mmf *MetadataNoteWolfyFileHandle) Save() error { + file, err := mmf.getMetadataFile() + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + if err := encoder.Encode(mmf); err != nil { + return err + } + return nil +} + +func (mmf *MetadataNoteWolfyFileHandle) AddChild(childNode *Node) error { + activeNodeName := mmf.ActiveNode + if activeNodeName == "" { + return errors.New("no active node, seems like you have not created a workspace yet") + } + + activeNode := mmf.FindNode(activeNodeName) + + activePath := activeNode.Path + childPath := childNode.Path + _, err := utility.DoesChildPathMatchesParentPath(activePath, childPath) + if err != nil { + return err + } + activeNode.Children = append(activeNode.Children, childNode) + + return nil +} + +func (mmf *MetadataNoteWolfyFileHandle) DeleteChildByIndex(index int) error { + activeNodeName := mmf.ActiveNode + if activeNodeName == "" { + return errors.New("no active node, seems like you have not created a workspace yet") + } + + activeNode := mmf.FindNode(activeNodeName) + if index < 0 || index >= len(activeNode.Children) { + return errors.New("index is out of range, check that the child at this index exists") + } + activeNode.Children = append(activeNode.Children[:index], activeNode.Children[index+1:]...) + + return nil +} + +func (mmf *MetadataNoteWolfyFileHandle) AddMarkdown(markdown *Markdown) error { + activeNodeName := mmf.ActiveNode + if activeNodeName == "" { + return errors.New("no active node, seems like you have not created a workspace yet") + } + + activeNode := mmf.FindNode(activeNodeName) + activeNode.Markdowns = append(activeNode.Markdowns, markdown) + + return nil +} + +func (mmf *MetadataNoteWolfyFileHandle) DeleteMarkdown(markdownName string) error { + activeNodeName := mmf.ActiveNode + if activeNodeName == "" { + return errors.New("no active node, seems like you have not created a workspace yet") + } + + activeNode := mmf.FindNode(activeNodeName) + + foundIndex := -1 + for index, markdown := range activeNode.Markdowns { + if markdown.Filename[:len(markdown.Filename)-3] == markdownName { + foundIndex = index + break + } + } + + if foundIndex == -1 { + return fmt.Errorf("markdownName %s matches no name of a markdown note", markdownName) + } + + activeNode.Markdowns = append(activeNode.Markdowns[:foundIndex], activeNode.Markdowns[foundIndex+1:]...) + return nil +} + +func (mmf *MetadataNoteWolfyFileHandle) ListWorkspaces() { + longestStringLength := len("Workspace Name") + for _, workspace := range mmf.Workspaces { + workspaceNameLength := len(workspace.Name) + if workspaceNameLength > longestStringLength { + longestStringLength = workspaceNameLength + } + workspacePathLength := len(workspace.Path) + if workspacePathLength > longestStringLength { + longestStringLength = workspacePathLength + } + } + + fmt.Printf(fmt.Sprintf("\r\n%%-%[1]d.%[1]ds%%-%[1]d.%[1]ds", longestStringLength), "Workspace Name", "Workspace Path") + totalWidth := 2*longestStringLength + 1 + fmt.Printf("\r\n%s\n", strings.Repeat("-", totalWidth)) + for _, workspace := range mmf.Workspaces { + fmt.Printf(fmt.Sprintf("\r%%-%[1]d.%[1]ds%%-%[1]d.%[1]ds\n", longestStringLength), workspace.Name, workspace.Path) + } +} + +func (mmf *MetadataNoteWolfyFileHandle) ListResourcesOnNode(node *Node) { + fmt.Println("\r\nYou are on node: ", node.Name) + fmt.Println("\rChild nodes:") + for _, child := range node.Children { + fmt.Println("\r", child.Name) + } + fmt.Println("\rMarkdown files:") + for _, markdown := range node.Markdowns { + fmt.Println("\r", markdown.Filename) + } +} + +func (mmf *MetadataNoteWolfyFileHandle) FindNode(name string) *Node { + activeWorkspaceName := mmf.ActiveWorkspace + var activeWorkspace *Node + for _, workspace := range mmf.Workspaces { + if workspace.Name == activeWorkspaceName { + activeWorkspace = workspace + } + } + if activeWorkspace == nil { + return nil + } + + queue := NewQueue[*Node]() + queue.Add(activeWorkspace) + + var node *Node + for queue.Len() > 0 { + currentNode := queue.Drop() + if currentNode.Name == name { + node = currentNode + break + } + for _, child := range currentNode.Children { + queue.Add(child) + } + } + + return node +} + +func (mmf *MetadataNoteWolfyFileHandle) FindParentNode(name string) *Node { + activeWorkspaceName := mmf.ActiveWorkspace + var activeWorkspace *Node + for _, workspace := range mmf.Workspaces { + if workspace.Name == activeWorkspaceName { + activeWorkspace = workspace + } + } + if activeWorkspace == nil { + return nil + } + + queue := NewQueue[*Node]() + queue.Add(activeWorkspace) + + var parentNode *Node + for queue.Len() > 0 { + currentNode := queue.Drop() + for _, childNode := range currentNode.Children { + if childNode.Name == name { + parentNode = currentNode + break + } + queue.Add(childNode) + } + } + + return parentNode +} + +func (mmf *MetadataNoteWolfyFileHandle) load() error { + file, err := mmf.getMetadataFile() + if err != nil { + return err + } + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + return err + } + if fileInfo.Size() == 0 { + return nil + } + + decoder := json.NewDecoder(file) + if err := decoder.Decode(mmf); err != nil { + return err + } + return nil +} + +func (mmf *MetadataNoteWolfyFileHandle) getMetadataFile() (*os.File, error) { + metadataFilePath := mmf.Config.MetadataFilePath + + file, err := os.OpenFile(metadataFilePath, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return nil, err + } + return file, nil +} diff --git a/internal/structure/metadata_test.go b/internal/structure/metadata_test.go new file mode 100644 index 0000000..990d117 --- /dev/null +++ b/internal/structure/metadata_test.go @@ -0,0 +1,573 @@ +//go:build unit_test + +package structure_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "reflect" + "testing" + + "github.com/RaphSku/notewolfy/internal/structure" + "github.com/RaphSku/notewolfy/internal/utility" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func CleanUpFile(filePath string) { + err := os.Remove(filePath) + if err != nil { + fmt.Println("Could not remove metadata file! Please clean it up yourself!") + os.Exit(1) + } +} + +func captureStdOutput(f func()) (string, error) { + originalStdOut := os.Stdout + r, w, err := os.Pipe() + if err != nil { + return "", err + } + os.Stdout = w + + outputC := make(chan string) + go func() { + var buf bytes.Buffer + io.Copy(&buf, r) + outputC <- buf.String() + }() + + f() + w.Close() + + os.Stdout = originalStdOut + out := <-outputC + + return out, nil +} + +func TestNewMetadataNoteWolfyFileHandle(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + var expMmf structure.MetadataNoteWolfyFileHandle + expMmf.Config = config + + mmf1, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + mmf2, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + assert.Equal(t, mmf1, mmf2) + + assert.Equal(t, expMmf.Config, mmf1.Config) + assert.Equal(t, expMmf.Workspaces, mmf1.Workspaces) + assert.Equal(t, expMmf.ActiveWorkspace, mmf1.ActiveWorkspace) +} + +func TestCreateNewWorkspace(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + expRelativeWorkspacePath := "~/tmp" + err = mmf.AddNewWorkspace("test", expRelativeWorkspacePath) + assert.NoError(t, err) + + expWorkspacePath, err := utility.ExpandRelativePaths(expRelativeWorkspacePath) + assert.NoError(t, err) + expWorkspaceNode := &structure.Node{ + Name: "test", + Path: expWorkspacePath, + } + expWorkspaceList := []*structure.Node{expWorkspaceNode} + assert.Equal(t, expWorkspaceList, mmf.Workspaces) + assert.Equal(t, expWorkspaceList[0].Path, mmf.Workspaces[0].Path) +} + +func TestDoesWorkspaceExist(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + expExists := false + actExists := mmf.DoesWorkspaceExist("test") + assert.Equal(t, expExists, actExists) + + err = mmf.AddNewWorkspace("test", "~/tmp") + assert.NoError(t, err) + expExists = true + actExists = mmf.DoesWorkspaceExist("test") + assert.Equal(t, expExists, actExists) +} + +func TestSave(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + err = mmf.AddNewWorkspace("testA", "~/tmp") + assert.NoError(t, err) + err = mmf.AddNewWorkspace("testB", "~/A/B") + assert.NoError(t, err) + + err = mmf.Save() + assert.NoError(t, err) + + file, err := os.Open(metadataFilePath) + assert.NoError(t, err) + decoder := json.NewDecoder(file) + var actMmf structure.MetadataNoteWolfyFileHandle + err = decoder.Decode(&actMmf) + assert.NoError(t, err) + + for i := range mmf.Workspaces { + assert.Equal(t, mmf.Workspaces[i].Name, actMmf.Workspaces[i].Name) + } + assert.Equal(t, mmf.ActiveWorkspace, actMmf.ActiveWorkspace) + assert.Equal(t, mmf.ActiveNode, actMmf.ActiveNode) +} + +func TestAddChildToNodeFailure(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + childNode := &structure.Node{ + Name: "B", + Path: "/B", + } + + err = mmf.AddChild(childNode) + if assert.Error(t, err) { + expError := errors.New("no active node, seems like you have not created a workspace yet") + assert.Equal(t, expError, err) + } + + node := &structure.Node{ + Name: "A", + Path: "/A", + } + mmf.ActiveNode = node.Name + mmf.ActiveWorkspace = node.Name + mmf.Workspaces = append(mmf.Workspaces, node) + err = mmf.AddChild(childNode) + if assert.Error(t, err) { + expError := errors.New("child's parent path and parentPath do not match") + assert.Equal(t, expError, err) + } +} + +func TestAddChildToNodeSuccess(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + var markdowns []*structure.Markdown + var children []*structure.Node + node := &structure.Node{ + Name: "A", + Path: "~/A", + Markdowns: markdowns, + Children: children, + } + mmf.Workspaces = append(mmf.Workspaces, node) + mmf.ActiveNode = node.Name + mmf.ActiveWorkspace = node.Name + + childNode := &structure.Node{ + Name: "B", + Path: "~/A/B", + Markdowns: markdowns, + Children: children, + } + err = mmf.AddChild(childNode) + assert.NoError(t, err) + actNode := mmf.FindNode(node.Name) + assert.Equal(t, actNode.Children[0], childNode) + mmf.Save() +} + +func TestDeleteChildByIndex(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + nodeA := &structure.Node{ + Name: "A", + Path: "~/test/A", + } + nodeB := &structure.Node{ + Name: "B", + Path: "~/test/B", + } + nodeC := &structure.Node{ + Name: "C", + Path: "~/test/C", + } + workspaceNode := &structure.Node{ + Name: "Test", + Path: "~/test", + Children: []*structure.Node{nodeA, nodeB, nodeC}, + } + + err = mmf.DeleteChildByIndex(0) + if assert.Error(t, err) { + expErr := errors.New("no active node, seems like you have not created a workspace yet") + assert.Equal(t, expErr, err) + } + + mmf.ActiveNode = workspaceNode.Name + mmf.ActiveWorkspace = workspaceNode.Name + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + + err = mmf.DeleteChildByIndex(-1) + if assert.Error(t, err) { + expErr := errors.New("index is out of range, check that the child at this index exists") + assert.Equal(t, expErr, err) + } + + err = mmf.DeleteChildByIndex(4) + if assert.Error(t, err) { + expErr := errors.New("index is out of range, check that the child at this index exists") + assert.Equal(t, expErr, err) + } + + err = mmf.DeleteChildByIndex(1) + expLength := 2 + assert.NoError(t, err) + assert.Equal(t, expLength, len(mmf.Workspaces[0].Children)) + assert.True(t, reflect.DeepEqual(nodeA, mmf.Workspaces[0].Children[0])) + assert.True(t, reflect.DeepEqual(nodeC, mmf.Workspaces[0].Children[1])) +} + +func TestAddMarkdown(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + var markdowns []*structure.Markdown + var children []*structure.Node + node := &structure.Node{ + Name: "test", + Path: "/A", + Markdowns: markdowns, + Children: children, + } + mmf.Workspaces = append(mmf.Workspaces, node) + mmf.ActiveNode = node.Name + mmf.ActiveWorkspace = node.Name + + markdown := &structure.Markdown{ + Filename: "test.md", + } + err = mmf.AddMarkdown(markdown) + assert.NoError(t, err) + actNode := mmf.FindNode(node.Name) + assert.Equal(t, actNode.Markdowns[0].Filename, markdown.Filename) +} + +func TestDeleteMarkdown(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + var markdowns []*structure.Markdown + var children []*structure.Node + node := &structure.Node{ + Name: "test", + Path: "/A", + Markdowns: markdowns, + Children: children, + } + mmf.ActiveNode = node.Name + mmf.ActiveWorkspace = node.Name + mmf.Workspaces = append(mmf.Workspaces, node) + + markdownName := "test" + markdown := &structure.Markdown{ + Filename: fmt.Sprintf("%s.md", markdownName), + } + err = mmf.AddMarkdown(markdown) + assert.NoError(t, err) + + err = mmf.DeleteMarkdown(markdownName) + assert.NoError(t, err) + actNode := mmf.FindNode(node.Name) + assert.Empty(t, actNode.Markdowns) +} + +func TestListWorkspaces(t *testing.T) { + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + var workspaceNodes []*structure.Node + var markdowns []*structure.Markdown + var children []*structure.Node + workspaceNodes = append(workspaceNodes, &structure.Node{ + Name: "A", + Path: "./tmp/A", + Markdowns: markdowns, + Children: children, + }) + workspaceNodes = append(workspaceNodes, &structure.Node{ + Name: "B", + Path: "./tmp/B", + Markdowns: markdowns, + Children: children, + }) + mmf.Workspaces = workspaceNodes + + actOutput, err := captureStdOutput(func() { + mmf.ListWorkspaces() + }) + assert.NoError(t, err) + + expOutput := "\r\nWorkspace NameWorkspace Path\r\n-----------------------------\n\rA ./tmp/A \n\rB ./tmp/B \n" + assert.Equal(t, expOutput, actOutput) +} + +func TestListResourcesOnNode(t *testing.T) { + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + var markdowns []*structure.Markdown + markdown := &structure.Markdown{ + Filename: "test", + } + markdowns = append(markdowns, markdown) + + var children []*structure.Node + childNode := &structure.Node{ + Name: "childTest", + Path: "/A/B", + } + children = append(children, childNode) + + node := &structure.Node{ + Name: "test", + Path: "/A", + Markdowns: markdowns, + Children: children, + } + + mmf.ActiveNode = node.Name + mmf.ActiveWorkspace = node.Name + mmf.Workspaces = append(mmf.Workspaces, node) + + actOutput, err := captureStdOutput(func() { + mmf.ListResourcesOnNode(node) + }) + assert.NoError(t, err) + + expOutput := "\r\nYou are on node: test\n\rChild nodes:\n\r childTest\n\rMarkdown files:\n\r test\n" + assert.Equal(t, expOutput, actOutput) +} + +func TestFindNode(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + var childrenOfB []*structure.Node + nodeD := &structure.Node{ + Name: "D", + Path: "/A/B/D", + } + childrenOfB = append(childrenOfB, nodeD) + nodeB := &structure.Node{ + Name: "B", + Path: "/A/B", + Children: childrenOfB, + } + nodeC := &structure.Node{ + Name: "C", + Path: "/A/C", + } + var children []*structure.Node + children = append(children, nodeB) + children = append(children, nodeC) + nodeA := &structure.Node{ + Name: "A", + Path: "/A", + Children: children, + } + mmf.Workspaces = append(mmf.Workspaces, nodeA) + mmf.ActiveWorkspace = nodeA.Name + mmf.ActiveNode = nodeA.Name + + actNode := mmf.FindNode("D") + assert.True(t, reflect.DeepEqual(nodeD, actNode)) +} + +func TestFindParentNode(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + var childrenOfB []*structure.Node + nodeD := &structure.Node{ + Name: "D", + Path: "/A/B/D", + } + childrenOfB = append(childrenOfB, nodeD) + nodeB := &structure.Node{ + Name: "B", + Path: "/A/B", + Children: childrenOfB, + } + nodeC := &structure.Node{ + Name: "C", + Path: "/A/C", + } + var children []*structure.Node + children = append(children, nodeB) + children = append(children, nodeC) + nodeA := &structure.Node{ + Name: "A", + Path: "/A", + Children: children, + } + mmf.Workspaces = append(mmf.Workspaces, nodeA) + mmf.ActiveWorkspace = nodeA.Name + mmf.ActiveNode = nodeA.Name + + actParentNode := mmf.FindParentNode("D") + assert.True(t, reflect.DeepEqual(nodeB, actParentNode)) +} + +func TestDecodingWhileLoading(t *testing.T) { + t.Parallel() + + uuid := uuid.New().String() + metadataFilePath := fmt.Sprintf("./.notewolfy-%s.json", uuid) + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + activeNode := &structure.Node{ + Name: "Active", + Path: "/A", + } + mmf.ActiveWorkspace = activeNode.Name + mmf.ActiveNode = activeNode.Name + mmf.Workspaces = append(mmf.Workspaces, activeNode) + + err = mmf.Save() + assert.NoError(t, err) + + newConfig := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + newMmf, err := structure.NewMetadataNoteWolfyFileHandle(newConfig) + assert.NoError(t, err) + + assert.True(t, reflect.DeepEqual(activeNode, newMmf.Workspaces[0])) +} diff --git a/internal/structure/queue.go b/internal/structure/queue.go new file mode 100644 index 0000000..b216de7 --- /dev/null +++ b/internal/structure/queue.go @@ -0,0 +1,28 @@ +package structure + +type Queue[T any] struct { + items []T +} + +func NewQueue[T any]() *Queue[T] { + return &Queue[T]{} +} + +func (q *Queue[T]) Len() int { + return len(q.items) +} + +func (q *Queue[T]) Add(item T) { + q.items = append(q.items, item) +} + +func (q *Queue[T]) Drop() T { + if q.Len() == 0 { + var zero T + return zero + } + item := q.items[0] + q.items = q.items[1:] + + return item +} diff --git a/internal/structure/queue_test.go b/internal/structure/queue_test.go new file mode 100644 index 0000000..6653b9d --- /dev/null +++ b/internal/structure/queue_test.go @@ -0,0 +1,62 @@ +//go:build unit_test + +package structure_test + +import ( + "testing" + + "github.com/RaphSku/notewolfy/internal/structure" + "github.com/stretchr/testify/assert" +) + +func TestNewQueue(t *testing.T) { + t.Parallel() + + expQueue := &structure.Queue[string]{} + actQueue := structure.NewQueue[string]() + assert.Equal(t, expQueue, actQueue) +} + +func TestAddingAndRemovingItemToQueue(t *testing.T) { + t.Parallel() + + expItem := "test" + queue := structure.NewQueue[string]() + queue.Add(expItem) + + actItem := queue.Drop() + assert.Equal(t, expItem, actItem) +} + +func TestQueueLen(t *testing.T) { + t.Parallel() + + queue := structure.NewQueue[string]() + queue.Add("testA") + queue.Add("testB") + queue.Add("testC") + + expLength := 3 + assert.Equal(t, expLength, queue.Len()) +} + +func TestFiFoProperty(t *testing.T) { + t.Parallel() + + expItem := "testA" + queue := structure.NewQueue[string]() + queue.Add(expItem) + queue.Add("testB") + + actItem := queue.Drop() + assert.Equal(t, expItem, actItem) +} + +func TestQueueDropOnZeroLength(t *testing.T) { + t.Parallel() + + queue := structure.NewQueue[string]() + actResult := queue.Drop() + expResult := "" + assert.Equal(t, expResult, actResult) +} diff --git a/internal/utility/table.go b/internal/utility/table.go new file mode 100644 index 0000000..b2b3055 --- /dev/null +++ b/internal/utility/table.go @@ -0,0 +1,80 @@ +package utility + +import ( + "errors" + "fmt" + "strings" +) + +type Table struct { + width int + headers []string + entries [][]string +} + +func NewTable(width int) *Table { + return &Table{ + width: width, + } +} + +func (t *Table) SetHeaders(headers []string) { + t.headers = append(t.headers, headers...) +} + +func (t *Table) Append(entriesInRow []string) error { + if len(entriesInRow) != len(t.headers) { + return errors.New("could not append entries because the length does not match") + } + t.entries = append(t.entries, entriesInRow) + + return nil +} + +func (t *Table) GenerateTable() error { + if len(t.headers) == 0 { + return errors.New("please set headers in order to generate the table") + } + + for index, header := range t.headers { + centeredHeader := t.centerString(header, t.width) + fmt.Print(centeredHeader) + if index != len(t.headers)-1 { + fmt.Print("|") + } + } + fmt.Print("\r\n") + divisionLine := t.divisionLine() + fmt.Print(divisionLine) + fmt.Print("\r\n") + for _, entriesInRow := range t.entries { + for j, entry := range entriesInRow { + centeredEntry := t.centerString(entry, t.width) + fmt.Print(centeredEntry) + if j != len(t.headers)-1 { + fmt.Print("|") + } + } + fmt.Print("\r\n") + } + fmt.Print("\r\n") + + return nil +} + +func (t *Table) centerString(target string, width int) string { + padding := width - len(target) + if padding <= 0 { + return target + } + leftPadding := padding / 2 + rightPadding := padding - leftPadding + + return strings.Repeat(" ", leftPadding) + target + strings.Repeat(" ", rightPadding) +} + +func (t *Table) divisionLine() string { + totalWidth := len(t.headers)*t.width + len(t.headers) - 1 + + return strings.Repeat("-", totalWidth) +} diff --git a/internal/utility/table_test.go b/internal/utility/table_test.go new file mode 100644 index 0000000..1b9d256 --- /dev/null +++ b/internal/utility/table_test.go @@ -0,0 +1,99 @@ +//go:build unit_test + +package utility_test + +import ( + "bytes" + "errors" + "io" + "os" + "testing" + + "github.com/RaphSku/notewolfy/internal/utility" + "github.com/stretchr/testify/assert" +) + +func captureStdOutput(f func()) (string, error) { + originalStdOut := os.Stdout + r, w, err := os.Pipe() + if err != nil { + return "", err + } + os.Stdout = w + + outputC := make(chan string) + go func() { + var buf bytes.Buffer + io.Copy(&buf, r) + outputC <- buf.String() + }() + + f() + w.Close() + + os.Stdout = originalStdOut + out := <-outputC + + return out, nil +} + +func TestGenerateTable(t *testing.T) { + expHeaders := []string{"testA", "testB", "testC"} + expEntries := [][]string{ + {"A", "B", "C"}, + {"hello", "world", "everybody"}, + } + + table := utility.NewTable(20) + table.SetHeaders(expHeaders) + for _, entryInRow := range expEntries { + err := table.Append(entryInRow) + assert.NoError(t, err) + } + + actTable, err := captureStdOutput(func() { + err := table.GenerateTable() + assert.NoError(t, err) + }) + assert.NoError(t, err) + expTable := " testA | testB | testC \r\n--------------------------------------------------------------\r\n A | B | C \r\n hello | world | everybody \r\n\r\n" + assert.Equal(t, expTable, actTable) +} + +func TestEntryLengthNotMatching(t *testing.T) { + headers := []string{"testA", "testB", "testC"} + faultyEntry := []string{"A", "B"} + + table := utility.NewTable(20) + table.SetHeaders(headers) + + err := table.Append(faultyEntry) + expError := errors.New("could not append entries because the length does not match") + if assert.Error(t, err) { + assert.Equal(t, expError, err) + } +} + +func TestGenerateTableWithNoHeaders(t *testing.T) { + table := utility.NewTable(20) + err := table.GenerateTable() + expError := errors.New("please set headers in order to generate the table") + if assert.Error(t, err) { + assert.Equal(t, expError, err) + } +} + +func TestCenterStringWithTooNarrowPadding(t *testing.T) { + headers := []string{"testA", "testB", "testC"} + entry := []string{"And here", "we go again", "too bad"} + table := utility.NewTable(5) + table.SetHeaders(headers) + table.Append(entry) + actOutput, err := captureStdOutput(func() { + err := table.GenerateTable() + assert.NoError(t, err) + }) + assert.NoError(t, err) + expOutput := "testA|testB|testC\r\n-----------------\r\nAnd here|we go again|too bad\r\n\r\n" + assert.Equal(t, expOutput, actOutput) +} diff --git a/internal/utility/utility.go b/internal/utility/utility.go new file mode 100644 index 0000000..d2b229a --- /dev/null +++ b/internal/utility/utility.go @@ -0,0 +1,52 @@ +package utility + +import ( + "errors" + "os" + "path/filepath" + "regexp" +) + +func GetHomeDir() (string, error) { + userHomeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + + return userHomeDir, nil +} + +func ExpandRelativePaths(path string) (string, error) { + expandedPath := path + if path[0] == '~' { + homeDir, err := GetHomeDir() + if err != nil { + return "", err + } + expandedPath = filepath.Join(homeDir, path[1:]) + } else if path[:2] == ".." || path[0] == '.' { + var err error + expandedPath, err = filepath.Abs(path) + if err != nil { + return "", err + } + } + + return expandedPath, nil +} + +func DoesChildPathMatchesParentPath(parentPath string, childPath string) (bool, error) { + re, err := regexp.Compile("/[\\w.]+") + if err != nil { + return false, err + } + + matches := re.FindAllString(childPath, -1) + childDirectoryLength := len(matches[len(matches)-1]) + childsParentPath := childPath[:len(childPath)-childDirectoryLength] + if childsParentPath != parentPath { + return false, errors.New("child's parent path and parentPath do not match") + } + + return true, nil +} diff --git a/internal/utility/utility_test.go b/internal/utility/utility_test.go new file mode 100644 index 0000000..e438b85 --- /dev/null +++ b/internal/utility/utility_test.go @@ -0,0 +1,87 @@ +//go:build unit_test + +package utility_test + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/RaphSku/notewolfy/internal/utility" + "github.com/stretchr/testify/assert" +) + +func TestGetHomeDir(t *testing.T) { + t.Parallel() + + expHomeDir, err := os.UserHomeDir() + assert.NoError(t, err) + + actHomeDir, err := utility.GetHomeDir() + assert.NoError(t, err) + + assert.Equal(t, expHomeDir, actHomeDir) +} + +func TestExpandRelativePaths(t *testing.T) { + t.Parallel() + + expHomeDir, err := os.UserHomeDir() + assert.NoError(t, err) + + expTildePath := filepath.Join(expHomeDir, "/tmp") + expDotPath, err := filepath.Abs("./tmp") + assert.NoError(t, err) + expPrevPath, err := filepath.Abs("../tmp") + assert.NoError(t, err) + + testCases := []struct { + name string + givenPath string + expPath string + }{ + {name: "Check tilde relative path", givenPath: "~/tmp", expPath: expTildePath}, + {name: "Check dot relative path", givenPath: "./tmp", expPath: expDotPath}, + {name: "Check previous directory relative path", givenPath: "../tmp", expPath: expPrevPath}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actPath, err := utility.ExpandRelativePaths(tc.givenPath) + assert.NoError(t, err) + assert.Equal(t, tc.expPath, actPath) + }) + } +} + +func TestDoesChildPathMatchesParentPath(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + givenPaths []string + expResult bool + expError bool + }{ + {name: "Check paths matching with tilde", givenPaths: []string{"~/tmp", "~/tmp/A"}, expResult: true, expError: false}, + {name: "Check paths matching with dot", givenPaths: []string{"./tmp", "./tmp/A"}, expResult: true, expError: false}, + {name: "Check paths matching", givenPaths: []string{"/A/B", "/A/B/C"}, expResult: true, expError: false}, + {name: "Check paths do not match", givenPaths: []string{"/A", "/B"}, expResult: false, expError: true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ok, err := utility.DoesChildPathMatchesParentPath(tc.givenPaths[0], tc.givenPaths[1]) + if tc.expError { + if assert.Error(t, err) { + expError := errors.New("child's parent path and parentPath do not match") + assert.Equal(t, expError, err) + } + return + } + assert.Equal(t, tc.expResult, ok) + assert.NoError(t, err) + }) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..103f793 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "context" + "os" + + "github.com/RaphSku/notewolfy/cmd" + "github.com/RaphSku/notewolfy/internal/logging" + "go.uber.org/zap" +) + +func main() { + // if debugLevel = 1, then debug logs are shown to os.Stdout, otherwise no logs will be printed + debugLevel := os.Getenv("NOTEWOLFY_DEBUG") + var logger *zap.Logger + if debugLevel == "1" { + logger = logging.SetupZapLogger(true) + } else { + logger = logging.SetupZapLogger(false) + } + + cli := cmd.NewCLI(context.Background(), logger) + cli.AddSubCommands() + cli.Execute() +} diff --git a/tests/integration/node_test.go b/tests/integration/node_test.go new file mode 100644 index 0000000..ca8ad2d --- /dev/null +++ b/tests/integration/node_test.go @@ -0,0 +1,104 @@ +//go:build integration_test + +package integration_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/RaphSku/notewolfy/internal/commands" + "github.com/RaphSku/notewolfy/internal/structure" + "github.com/RaphSku/notewolfy/internal/utility" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func createUniquePath(path string) string { + uuid := uuid.New().String() + + return fmt.Sprintf("%s-%s", path, uuid) +} + +func CleanUpFile(filePath string) { + err := os.Remove(filePath) + if err != nil { + fmt.Println("Could not remove metadata file! Please clean it up yourself!") + os.Exit(1) + } +} + +func TestNodeCreatingAndDeleting(t *testing.T) { + // Scenario: + // 1. Prepare workspace + // 2. Create test node + // 3. Create another node + // 4. Use command `goto` to move to test node + // 5. Use command `goback` to move back to the workspace node + // 6. Delete test node + t.Parallel() + + metadataFilePath := createUniquePath("./.notewolfy") + config := &structure.Config{ + MetadataFilePath: metadataFilePath, + } + defer CleanUpFile(metadataFilePath) + + mmf, err := structure.NewMetadataNoteWolfyFileHandle(config) + assert.NoError(t, err) + + workspacePath := createUniquePath("./tmp") + + // 1. Prepare workspace + workspacePath, err = utility.ExpandRelativePaths(workspacePath) + assert.NoError(t, err) + err = os.Mkdir(workspacePath, os.ModePerm) + defer os.RemoveAll(workspacePath) + assert.NoError(t, err) + workspaceNode := &structure.Node{ + Name: "Workspace", + Path: workspacePath, + } + mmf.Workspaces = append(mmf.Workspaces, workspaceNode) + mmf.ActiveNode = workspaceNode.Name + mmf.ActiveWorkspace = workspaceNode.Name + err = mmf.Save() + assert.NoError(t, err) + + // 2. Create test node + testNodeName := "Test" + statement := fmt.Sprintf("create node %s", testNodeName) + commands.MatchStatementToCommand(mmf, statement) + testNodePath := filepath.Join(workspacePath, testNodeName) + assert.DirExists(t, testNodePath) + assert.Equal(t, testNodeName, mmf.Workspaces[0].Children[0].Name) + + // 3. Create another node + nodeName := "A" + statement = fmt.Sprintf("create node %s", nodeName) + commands.MatchStatementToCommand(mmf, statement) + nodePath := filepath.Join(workspacePath, testNodeName) + defer os.Remove(nodePath) + assert.DirExists(t, nodePath) + + // 4. Use command `goto` to move to test node + statement = fmt.Sprintf("goto %s", testNodeName) + commands.MatchStatementToCommand(mmf, statement) + assert.Equal(t, testNodeName, mmf.ActiveNode) + + // 5. Use command `goback` to move back to the workspace node + statement = "goback" + commands.MatchStatementToCommand(mmf, statement) + assert.Equal(t, workspaceNode.Name, mmf.ActiveNode) + + // 6. Delete test node + statement = fmt.Sprintf("delete node %s", testNodeName) + commands.MatchStatementToCommand(mmf, statement) + if _, err = os.Stat(testNodePath); err == nil { + os.Remove(testNodePath) + } + assert.NoDirExists(t, testNodePath) + assert.Equal(t, 1, len(mmf.Workspaces[0].Children)) + assert.Equal(t, nodeName, mmf.Workspaces[0].Children[0].Name) +}