From e26ac5133e052d0c08fc8df91bff33dea491f16c Mon Sep 17 00:00:00 2001 From: Leonardo Mello Date: Tue, 11 Apr 2023 12:22:06 -0300 Subject: [PATCH] Add `notebook` configuration to set default notebook path (#304) --- CHANGELOG.md | 1 + docs/config-notebook.md | 15 ++++++++ docs/config.md | 5 +++ docs/notebook.md | 2 ++ internal/cli/container.go | 18 +++++++++- internal/core/config.go | 62 +++++++++++++++++++++++---------- internal/core/config_test.go | 45 ++++++++++++++++++------ internal/core/notebook_store.go | 2 +- 8 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 docs/config-notebook.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f328aa9..6845a4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Added * New [`tool.shell`](docs/tool-shell.md) configuration key to set a custom shell (contributed by [@lsvmello](https://github.com/mickael-menu/zk/pull/302)). +* New [`notebook.dir`](docs/config-notebook.md) configuration key to set the default notebook (contributed by [@lsvmello](https://github.com/mickael-menu/zk/pull/304)). ## 0.13.0 diff --git a/docs/config-notebook.md b/docs/config-notebook.md new file mode 100644 index 0000000..ce76040 --- /dev/null +++ b/docs/config-notebook.md @@ -0,0 +1,15 @@ +# Notebook configuration + +The `[notebook]` section from the [configuration file](config.md) is used to set the default notebook directory. +If the path starts with `~` it will be replaced with the user home directory (`$HOME`). This property also supports environment variables. + +```toml +[notebook] +dir = "~/notebook" # same as "$HOME/notebook" +``` + + The following properties are customizable: + +* `dir` (string) + * Path of the default notebook. + * Only available in the global config file (`~/.config/zk/config.toml`). diff --git a/docs/config.md b/docs/config.md index 81a0bc6..77acdfe 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,6 +2,7 @@ Each [notebook](notebook.md) contains a configuration file used to customize your experience with `zk`. This file is located at `.zk/config.toml` and uses the [TOML format](https://github.com/toml-lang/toml). It is composed of several optional sections: +* `[notebook]` configures the [default notebook](config-notebook.md) * `[note]` sets the [note creation rules](config-note.md) * `[extra]` contains free [user variables](config-extra.md) which can be expanded in templates * `[group]` defines [note groups](config-group.md) with custom rules @@ -26,6 +27,10 @@ Notebook configuration files will inherit the settings defined in the global con Here's an example of a complete configuration file: ```toml +# NOTEBOOK SETTINGS +[notebook] +dir = "~/notebook" + # NOTE SETTINGS [note] diff --git a/docs/notebook.md b/docs/notebook.md index 4b02c73..bdda17b 100644 --- a/docs/notebook.md +++ b/docs/notebook.md @@ -6,6 +6,8 @@ To create a new notebook, simply run `zk init []`. Most `zk` commands are operating "Git-style" on the notebook containing the current working directory (or one of its parents). However, you can explicitly set which notebook to use with `--notebook-dir` or the `ZK_NOTEBOOK_DIR` environment variable. Setting `ZK_NOTEBOOK_DIR` in your shell configuration (e.g. `~/.profile`) can be used to define a default notebook which `zk` commands will use when the working directory is not in another notebook. +If the [default notebook](config-notebook.md) is set it will be used as `ZK_NOTEBOOK_DIR`, unless this environment variable is not already set. + ## Anatomy of a notebook Similarly to Git, a notebook is identified by the presence of a `.zk` directory at its root. This directory contains the only `zk`-specific files in your notebook: diff --git a/internal/cli/container.go b/internal/cli/container.go index eb19085..da590f3 100644 --- a/internal/cli/container.go +++ b/internal/cli/container.go @@ -4,6 +4,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/mickael-menu/zk/internal/adapter/editor" "github.com/mickael-menu/zk/internal/adapter/fs" @@ -65,12 +66,27 @@ func NewContainer(version string) (*Container, error) { return nil, wrap(err) } if configPath != "" { - config, err = core.OpenConfig(configPath, config, fs) + config, err = core.OpenConfig(configPath, config, fs, true) if err != nil { return nil, wrap(err) } } + // Set the default notebook if not already set + // might be overrided if --notebook-dir flag is present + if osutil.GetOptEnv("ZK_NOTEBOOK_DIR").IsNull() && !config.Notebook.Dir.IsNull() { + // Expand in case there are environment variables on the path + notebookDir := os.Expand(config.Notebook.Dir.Unwrap(), os.Getenv) + if strings.HasPrefix(notebookDir, "~") { + dirname, err := os.UserHomeDir() + if err != nil { + return nil, wrap(err) + } + notebookDir = filepath.Join(dirname, notebookDir[1:]) + } + os.Setenv("ZK_NOTEBOOK_DIR", notebookDir) + } + // Set the default shell if not already set if osutil.GetOptEnv("ZK_SHELL").IsNull() && !config.Tool.Shell.IsEmpty() { os.Setenv("ZK_SHELL", config.Tool.Shell.Unwrap()) diff --git a/internal/core/config.go b/internal/core/config.go index d6778dc..ffce238 100644 --- a/internal/core/config.go +++ b/internal/core/config.go @@ -12,19 +12,23 @@ import ( // Config holds the user configuration. type Config struct { - Note NoteConfig - Groups map[string]GroupConfig - Format FormatConfig - Tool ToolConfig - LSP LSPConfig - Filters map[string]string - Aliases map[string]string - Extra map[string]string + Notebook NotebookConfig + Note NoteConfig + Groups map[string]GroupConfig + Format FormatConfig + Tool ToolConfig + LSP LSPConfig + Filters map[string]string + Aliases map[string]string + Extra map[string]string } // NewDefaultConfig creates a new Config with the default settings. func NewDefaultConfig() Config { return Config{ + Notebook: NotebookConfig{ + Dir: opt.NullString, + }, Note: NoteConfig{ FilenameTemplate: "{{id}}", Extension: "md", @@ -192,6 +196,11 @@ const ( LSPDiagnosticHint LSPDiagnosticSeverity = 4 ) +// NotebookConfig holds configuration about the default notebook +type NotebookConfig struct { + Dir opt.String +} + // NoteConfig holds the user configuration used when generating new notes. type NoteConfig struct { // Handlebars template used when generating a new filename. @@ -249,7 +258,7 @@ func (c GroupConfig) Clone() GroupConfig { // OpenConfig creates a new Config instance from its TOML representation stored // in the given file. -func OpenConfig(path string, parentConfig Config, fs FileStorage) (Config, error) { +func OpenConfig(path string, parentConfig Config, fs FileStorage, isGlobal bool) (Config, error) { // The local config is optional. exists, err := fs.FileExists(path) if err == nil && !exists { @@ -261,7 +270,7 @@ func OpenConfig(path string, parentConfig Config, fs FileStorage) (Config, error return parentConfig, errors.Wrapf(err, "failed to open config file at %s", path) } - return ParseConfig(content, path, parentConfig) + return ParseConfig(content, path, parentConfig, isGlobal) } // ParseConfig creates a new Config instance from its TOML representation. @@ -269,7 +278,7 @@ func OpenConfig(path string, parentConfig Config, fs FileStorage) (Config, error // for templates. // // The parentConfig will be used to inherit default config settings. -func ParseConfig(content []byte, path string, parentConfig Config) (Config, error) { +func ParseConfig(content []byte, path string, parentConfig Config, isGlobal bool) (Config, error) { wrap := errors.Wrapperf("failed to read config") config := parentConfig @@ -280,6 +289,16 @@ func ParseConfig(content []byte, path string, parentConfig Config) (Config, erro return config, wrap(err) } + // Notebook + notebook := tomlConf.Notebook + if notebook.Dir != "" { + if isGlobal { + config.Notebook.Dir = opt.NewNotEmptyString(notebook.Dir) + } else { + return config, wrap(errors.New("notebook.dir should not be set on local configuration")) + } + } + // Note note := tomlConf.Note if note.Filename != "" { @@ -472,14 +491,19 @@ func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string) GroupConfig { // tomlConfig holds the TOML representation of Config type tomlConfig struct { - Note tomlNoteConfig - Groups map[string]tomlGroupConfig `toml:"group"` - Format tomlFormatConfig - Tool tomlToolConfig - LSP tomlLSPConfig - Extra map[string]string - Filters map[string]string `toml:"filter"` - Aliases map[string]string `toml:"alias"` + Notebook tomlNotebookConfig + Note tomlNoteConfig + Groups map[string]tomlGroupConfig `toml:"group"` + Format tomlFormatConfig + Tool tomlToolConfig + LSP tomlLSPConfig + Extra map[string]string + Filters map[string]string `toml:"filter"` + Aliases map[string]string `toml:"alias"` +} + +type tomlNotebookConfig struct { + Dir string } type tomlNoteConfig struct { diff --git a/internal/core/config_test.go b/internal/core/config_test.go index 2c534cf..57b9013 100644 --- a/internal/core/config_test.go +++ b/internal/core/config_test.go @@ -10,10 +10,13 @@ import ( ) func TestParseDefaultConfig(t *testing.T) { - conf, err := ParseConfig([]byte(""), ".zk/config.toml", NewDefaultConfig()) + conf, err := ParseConfig([]byte(""), ".zk/config.toml", NewDefaultConfig(), true) assert.Nil(t, err) assert.Equal(t, conf, Config{ + Notebook: NotebookConfig{ + Dir: opt.NullString, + }, Note: NoteConfig{ FilenameTemplate: "{{id}}", Extension: "md", @@ -58,7 +61,7 @@ func TestParseDefaultConfig(t *testing.T) { } func TestParseInvalidConfig(t *testing.T) { - _, err := ParseConfig([]byte(`;`), ".zk/config.toml", NewDefaultConfig()) + _, err := ParseConfig([]byte(`;`), ".zk/config.toml", NewDefaultConfig(), false) assert.NotNil(t, err) } @@ -66,6 +69,9 @@ func TestParseComplete(t *testing.T) { conf, err := ParseConfig([]byte(` # Comment + [notebook] + dir = "~/notebook" + [note] filename = "{{id}}.note" extension = "txt" @@ -138,10 +144,13 @@ func TestParseComplete(t *testing.T) { [lsp.diagnostics] wiki-title = "hint" dead-link = "none" - `), ".zk/config.toml", NewDefaultConfig()) + `), ".zk/config.toml", NewDefaultConfig(), true) assert.Nil(t, err) assert.Equal(t, conf, Config{ + Notebook: NotebookConfig{ + Dir: opt.NewString("~/notebook"), + }, Note: NoteConfig{ FilenameTemplate: "{{id}}.note", Extension: "txt", @@ -295,7 +304,7 @@ func TestParseMergesGroupConfig(t *testing.T) { log-ext = "value" [group.inherited] - `), ".zk/config.toml", NewDefaultConfig()) + `), ".zk/config.toml", NewDefaultConfig(), false) assert.Nil(t, err) assert.Equal(t, conf, Config{ @@ -394,7 +403,7 @@ func TestParsePreservePropertiesAllowingEmptyValues(t *testing.T) { [tool] pager = "" fzf-preview = "" - `), ".zk/config.toml", NewDefaultConfig()) + `), ".zk/config.toml", NewDefaultConfig(), false) assert.Nil(t, err) assert.Equal(t, conf.Tool.Pager.IsNull(), false) @@ -403,13 +412,29 @@ func TestParsePreservePropertiesAllowingEmptyValues(t *testing.T) { assert.Equal(t, conf.Tool.FzfPreview, opt.NewString("")) } +func TestParseNotebook(t *testing.T) { + toml := ` + [notebook] + dir = "/home/user/folder" + ` + // Should parse notebook if isGlobal == true + conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig(), true) + assert.Nil(t, err) + assert.Equal(t, conf.Notebook.Dir, opt.NewString("/home/user/folder")) + + // Should not parse notebook if isGlobal == false + conf, err = ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig(), false) + assert.NotNil(t, err) + assert.Err(t, err, "notebook.dir should not be set on local configuration") +} + func TestParseIDCharset(t *testing.T) { test := func(charset string, expected Charset) { toml := fmt.Sprintf(` [note] id-charset = "%v" `, charset) - conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig()) + conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig(), false) assert.Nil(t, err) if !cmp.Equal(conf.Note.IDOptions.Charset, expected) { t.Errorf("Didn't parse ID charset `%v` as expected", charset) @@ -430,7 +455,7 @@ func TestParseIDCase(t *testing.T) { [note] id-case = "%v" `, letterCase) - conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig()) + conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig(), false) assert.Nil(t, err) if !cmp.Equal(conf.Note.IDOptions.Case, expected) { t.Errorf("Didn't parse ID case `%v` as expected", letterCase) @@ -451,7 +476,7 @@ func TestParseMarkdownLinkEncodePath(t *testing.T) { [format.markdown] link-format = "%s" `, format) - conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig()) + conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig(), false) assert.Nil(t, err) assert.Equal(t, conf.Format.Markdown.LinkEncodePath, expected) } @@ -469,7 +494,7 @@ func TestParseLSPDiagnosticsSeverity(t *testing.T) { wiki-title = "%s" dead-link = "%s" `, value, value) - conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig()) + conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig(), false) assert.Nil(t, err) assert.Equal(t, conf.LSP.Diagnostics.WikiTitle, expected) assert.Equal(t, conf.LSP.Diagnostics.DeadLink, expected) @@ -486,7 +511,7 @@ func TestParseLSPDiagnosticsSeverity(t *testing.T) { [lsp.diagnostics] wiki-title = "foobar" ` - _, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig()) + _, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig(), false) assert.Err(t, err, "foobar: unknown LSP diagnostic severity - may be none, hint, info, warning or error") } diff --git a/internal/core/notebook_store.go b/internal/core/notebook_store.go index e258057..e957406 100644 --- a/internal/core/notebook_store.go +++ b/internal/core/notebook_store.go @@ -64,7 +64,7 @@ func (ns *NotebookStore) Open(path string) (*Notebook, error) { } configPath := filepath.Join(path, ".zk/config.toml") - config, err := OpenConfig(configPath, ns.config, ns.fs) + config, err := OpenConfig(configPath, ns.config, ns.fs, false) if err != nil { return nil, wrap(err) }