diff --git a/CHANGELOG.md b/CHANGELOG.md index 18eb772..06f6862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added +* Interactive wizard for the `zk init` command. * An experimental Language Server for LSP-compatible editors: * Auto-complete Markdown links with `[[` (setup wiki-links in the [note formats configuration](docs/note-format.md)) * Auto-complete [hashtags and colon-separated tags](docs/tags.md). @@ -27,6 +28,7 @@ All notable changes to this project will be documented in this file. * For convenience, `ZK_NOTEBOOK_DIR` behaves like setting a `--working-dir` fallback, instead of `--notebook-dir`. This way, paths will be relative to the root of the notebook. * A practical use case is to use `zk list -W .` when outside a notebook. This will list the notes in `ZK_NOTEBOOK_DIR` but print paths relative to the current directory, making them actionable from your terminal emulator. + ## 0.3.0 ### Added diff --git a/go.mod b/go.mod index 1815e60..2161240 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 replace github.com/tliron/glsp => github.com/mickael-menu/glsp v0.1.0 require ( - github.com/AlecAivazis/survey/v2 v2.2.7 + github.com/AlecAivazis/survey/v2 v2.2.12 github.com/alecthomas/kong v0.2.16-0.20210209082517-405b2f4fd9a4 github.com/aymerick/raymond v2.0.2+incompatible github.com/fatih/color v1.10.0 @@ -16,6 +16,7 @@ require ( github.com/lestrrat-go/strftime v1.0.4 github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-sqlite3 v1.14.6 + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mickael-menu/pretty v0.2.3 github.com/mvdan/xurls v1.1.0 github.com/pelletier/go-toml v1.8.1 @@ -28,5 +29,9 @@ require ( github.com/tliron/kutil v0.1.22 github.com/yuin/goldmark v1.3.2 github.com/yuin/goldmark-meta v1.0.0 + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect + golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect + golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect + golang.org/x/text v0.3.6 // indirect gopkg.in/djherbis/times.v1 v1.2.0 ) diff --git a/go.sum b/go.sum index 0417816..c6cba5d 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.2.7 h1:5NbxkF4RSKmpywYdcRgUmos1o+roJY8duCLZXbVjoig= github.com/AlecAivazis/survey/v2 v2.2.7/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk= +github.com/AlecAivazis/survey/v2 v2.2.12 h1:5a07y93zA6SZ09gOa9wLVLznF5zTJMQ+pJ3cZK4IuO8= +github.com/AlecAivazis/survey/v2 v2.2.12/go.mod h1:6d4saEvBsfSHXeN1a5OA5m2+HJ2LuVokllnC77pAIKI= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= @@ -369,6 +371,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mickael-menu/glsp v0.1.0 h1:we6mTssWXxGPVeEcTpCW8AOpdCuUXwUZ6Q2UiYVnCOw= github.com/mickael-menu/glsp v0.1.0/go.mod h1:ouzTGvQteTU4hdsG+32vIx0if7E9CzMa64d7tYJJ91g= github.com/mickael-menu/pretty v0.2.3 h1:AXi5WcBuWxwQV6iY/GhmCFpaoboQO2SLtzfujrn7dv0= @@ -561,6 +565,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -623,6 +629,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -683,9 +690,14 @@ golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= +golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -693,6 +705,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/cli/cmd/init.go b/internal/cli/cmd/init.go index 4db8ffa..b2d1085 100644 --- a/internal/cli/cmd/init.go +++ b/internal/cli/cmd/init.go @@ -4,7 +4,11 @@ import ( "fmt" "path/filepath" + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" "github.com/mickael-menu/zk/internal/cli" + "github.com/mickael-menu/zk/internal/core" + "github.com/mickael-menu/zk/internal/util/strings" ) // Init creates a notebook in the given directory @@ -13,7 +17,17 @@ type Init struct { } func (cmd *Init) Run(container *cli.Container) error { - notebook, err := container.Notebooks.Init(cmd.Directory) + opts, err := startInitWizard() + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return err + } + + fmt.Println() + + notebook, err := container.Notebooks.Init(cmd.Directory, opts) if err != nil { return err } @@ -32,3 +46,45 @@ func (cmd *Init) Run(container *cli.Container) error { fmt.Printf("Initialized a notebook in %v\n", path) return nil } + +func startInitWizard() (core.InitOpts, error) { + answers := struct { + WikiLink bool + Tags []string + }{} + + hashtag := "#hashtag" + multiwordTag := "#Bear's multi-word tag#" + colonTag := ":colon:tag:" + + questions := []*survey.Question{ + { + Name: "wikilink", + Prompt: &survey.Confirm{ + Message: "Do you prefer [[WikiLinks]] over regular Markdown links?", + Default: false, + }, + }, + { + Name: "tags", + Prompt: &survey.MultiSelect{ + Message: "Choose your favorite inline tag syntaxes:", + Options: []string{hashtag, multiwordTag, colonTag}, + }, + }, + } + + var opts core.InitOpts + err := survey.Ask(questions, &answers) + if err != nil { + return opts, err + } + + opts.WikiLinks = answers.WikiLink + + opts.Hashtags = strings.InList(answers.Tags, hashtag) + opts.MultiwordTags = strings.InList(answers.Tags, multiwordTag) + opts.ColonTags = strings.InList(answers.Tags, colonTag) + + return opts, nil +} diff --git a/internal/cli/container.go b/internal/cli/container.go index 369aa41..221cc12 100644 --- a/internal/cli/container.go +++ b/internal/cli/container.go @@ -70,6 +70,14 @@ func NewContainer(version string) (*Container, error) { FS: fs, Notebooks: core.NewNotebookStore(config, core.NotebookStorePorts{ FS: fs, + TemplateLoaderFactory: func(language string) (core.TemplateLoader, error) { + loader := handlebars.NewLoader(handlebars.LoaderOpts{ + LookupPaths: []string{}, + Styler: styler, + }) + + return loader, nil + }, NotebookFactory: func(path string, config core.Config) (*core.Notebook, error) { dbPath := filepath.Join(path, ".zk/notebook.db") db, err := sqlite.Open(dbPath) diff --git a/internal/core/notebook_store.go b/internal/core/notebook_store.go index d3f9373..48603f8 100644 --- a/internal/core/notebook_store.go +++ b/internal/core/notebook_store.go @@ -9,27 +9,30 @@ import ( // NotebookStore retrieves or creates new notebooks. type NotebookStore struct { - config Config - notebookFactory NotebookFactory - fs FileStorage + config Config + notebookFactory NotebookFactory + templateLoaderFactory TemplateLoaderFactory + fs FileStorage // Cached opened notebooks. notebooks map[string]*Notebook } type NotebookStorePorts struct { - NotebookFactory NotebookFactory - FS FileStorage + NotebookFactory NotebookFactory + TemplateLoaderFactory TemplateLoaderFactory + FS FileStorage } // NewNotebookStore creates a new NotebookStore instance using the given // options and port implementations. func NewNotebookStore(config Config, ports NotebookStorePorts) *NotebookStore { return &NotebookStore{ - config: config, - notebookFactory: ports.NotebookFactory, - fs: ports.FS, - notebooks: map[string]*Notebook{}, + config: config, + notebookFactory: ports.NotebookFactory, + templateLoaderFactory: ports.TemplateLoaderFactory, + fs: ports.FS, + notebooks: map[string]*Notebook{}, } } @@ -91,8 +94,16 @@ func (ns *NotebookStore) cachedNotebookAt(path string) *Notebook { return nil } +// InitOpts holds the user preferences when creating a new notebook. +type InitOpts struct { + WikiLinks bool + Hashtags bool + ColonTags bool + MultiwordTags bool +} + // Init creates a new notebook at the given file path. -func (ns *NotebookStore) Init(path string) (*Notebook, error) { +func (ns *NotebookStore) Init(path string, options InitOpts) (*Notebook, error) { wrap := errors.Wrapper("init") path, err := ns.fs.Abs(path) @@ -105,7 +116,11 @@ func (ns *NotebookStore) Init(path string) (*Notebook, error) { } // Create the default configuration file. - err = ns.fs.Write(filepath.Join(path, ".zk/config.toml"), []byte(defaultConfig)) + config, err := ns.generateConfig(options) + if err != nil { + return nil, wrap(err) + } + err = ns.fs.Write(filepath.Join(path, ".zk/config.toml"), []byte(config)) if err != nil { return nil, wrap(err) } @@ -144,6 +159,18 @@ func (ns *NotebookStore) locateNotebook(path string) (string, error) { return locate(path) } +func (ns *NotebookStore) generateConfig(options InitOpts) (string, error) { + loader, err := ns.templateLoaderFactory("en") + if err != nil { + return "", err + } + template, err := loader.LoadTemplate(defaultConfig) + if err != nil { + return "", err + } + return template.Render(options) +} + const defaultConfig = `# zk configuration file # # Uncomment the properties you want to customize. @@ -161,7 +188,7 @@ const defaultConfig = `# zk configuration file #default-title = "Untitled" # Template used to generate a note's filename, without extension. -#filename = "{{id}}" +#filename = "\{{id}}" # The file extension used for the notes. #extension = "md" @@ -190,7 +217,7 @@ template = "default.md" # EXTRA VARIABLES # # A dictionary of variables you can use for any custom values when generating -# new notes. They are accessible in templates with {{extra.}} +# new notes. They are accessible in templates with \{{extra.}} [extra] #key = "value" @@ -211,7 +238,7 @@ template = "default.md" #[group.""] #paths = ["", ""] #[group."".note] -#filename = "{{date now}}" +#filename = "\{{date now}}" #[group."".extra] #key = "value" @@ -221,7 +248,11 @@ template = "default.md" # Format used to generate links between notes. # Either "wiki", "markdown" or a custom template. Default is "markdown". +{{#if WikiLinks}} +link-format = "wiki" +{{else}} #link-format = "wiki" +{{/if}} # Indicates whether a link's path will be percent-encoded. # Defaults to true for "markdown" format and false for "wiki" format. #link-encode-path = true @@ -230,12 +261,24 @@ template = "default.md" #link-drop-extension = true # Enable support for #hashtags. -#hashtags = true +{{#if Hashtags}} +hashtags = true +{{else}} +hashtags = false +{{/if}} # Enable support for :colon:separated:tags:. -#colon-tags = true +{{#if ColonTags}} +colon-tags = true +{{else}} +colon-tags = false +{{/if}} # Enable support for Bear's #multi-word tags# # Hashtags must be enabled for multi-word tags to work. -#multiword-tags = true +{{#if MultiwordTags}} +multiword-tags = true +{{else}} +multiword-tags = false +{{/if}} # EXTERNAL TOOLS @@ -298,7 +341,7 @@ template = "default.md" # arguments. This can be useful to expand a complex search query into a flag # taking only paths. For example: # zk list --link-to "` + "`" + `zk path -m potatoe` + "`" + `" -#path = "zk list --quiet --format {{path}} --delimiter , $@" +#path = "zk list --quiet --format \{{path}} --delimiter , $@" # Show a random note. #lucky = "zk list --quiet --format full --sort random --limit 1"