Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add notebook configuration to set default notebook path #304

Merged
merged 3 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions docs/config-notebook.md
Original file line number Diff line number Diff line change
@@ -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`).
5 changes: 5 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]

Expand Down
2 changes: 2 additions & 0 deletions docs/notebook.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ To create a new notebook, simply run `zk init [<directory>]`.

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:
Expand Down
18 changes: 17 additions & 1 deletion internal/cli/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
Expand Down
62 changes: 43 additions & 19 deletions internal/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -261,15 +270,15 @@ 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.
// path is the config absolute path, from which will be derived the base path
// 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
Expand All @@ -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 != "" {
Expand Down Expand Up @@ -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 {
Expand Down
45 changes: 35 additions & 10 deletions internal/core/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -58,14 +61,17 @@ 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)
}

func TestParseComplete(t *testing.T) {
conf, err := ParseConfig([]byte(`
# Comment

[notebook]
dir = "~/notebook"

[note]
filename = "{{id}}.note"
extension = "txt"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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")
}

Expand Down
2 changes: 1 addition & 1 deletion internal/core/notebook_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down