Add `notebook` configuration to set default notebook path (#304)

pull/322/head
Leonardo Mello 1 year ago committed by GitHub
parent a8d1db4c57
commit e26ac5133e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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`).

@ -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]

@ -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:

@ -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())

@ -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 {

@ -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")
}

@ -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)
}

Loading…
Cancel
Save