Specify the notebook directory explicitly (#14)

This commit is contained in:
Mickaël Menu 2021-03-18 19:47:49 +01:00 committed by GitHub
parent be8b2d6289
commit e653c71356
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 75 deletions

View File

@ -11,6 +11,10 @@ All notable changes to this project will be documented in this file.
* This is the same format as a notebook [configuration file](docs/config.md).
* Shared templates can be stored in `~/.config/zk/templates/`.
* `XDG_CONFIG_HOME` is taken into account.
* Use `--notebook-dir` or set `ZK_NOTEBOOK_DIR` to run `zk` as if it was started from this path instead of the current working directory.
* This allows running `zk` without being in a notebook.
* By setting `ZK_NOTEBOOK_DIR` in your shell configuration file (e.g. `~/.profile`), you are declaring a default global notebook which will be used when `zk` is not in a notebook.
* When the notebook directory is set explicitly, any path given as argument will be relative to it instead of the actual working directory.
## 0.2.1

View File

@ -26,6 +26,7 @@ type Container struct {
Date date.Provider
Logger util.Logger
Terminal *term.Terminal
WorkingDir string
templateLoader *handlebars.Loader
zk *zk.Zk
zkErr error
@ -48,24 +49,15 @@ func NewContainer() (*Container, error) {
}
}
// Open current notebook
zk, zkErr := zk.Open(".", config)
if zkErr == nil {
config = zk.Config
os.Setenv("ZK_PATH", zk.Path)
}
date := date.NewFrozenNow()
return &Container{
Config: config,
Logger: util.NewStdLogger("zk: ", 0),
// zk is short-lived, so we freeze the current date to use the same
// date for any rendering during the execution.
// date for any template rendering during the execution.
Date: &date,
Logger: util.NewStdLogger("zk: ", 0),
Terminal: term.New(),
zk: zk,
zkErr: zkErr,
}, nil
}
@ -94,6 +86,24 @@ func locateGlobalConfig() (string, error) {
}
}
// OpenNotebook resolves and loads the first notebook found in the given
// searchPaths.
func (c *Container) OpenNotebook(searchPaths []string) {
if len(searchPaths) == 0 {
panic("no notebook search paths provided")
}
for _, path := range searchPaths {
c.zk, c.zkErr = zk.Open(path, c.Config)
if c.zkErr == nil {
c.WorkingDir = path
c.Config = c.zk.Config
os.Setenv("ZK_NOTEBOOK_DIR", c.zk.Path)
return
}
}
}
func (c *Container) Zk() (*zk.Zk, error) {
return c.zk, c.zkErr
}
@ -106,11 +116,11 @@ func (c *Container) TemplateLoader(lang string) *handlebars.Loader {
return c.templateLoader
}
func (c *Container) Parser(zk *zk.Zk) *markdown.Parser {
func (c *Container) Parser() *markdown.Parser {
return markdown.NewParser(markdown.ParserOpts{
HashtagEnabled: zk.Config.Format.Markdown.Hashtags,
MultiWordTagEnabled: zk.Config.Format.Markdown.MultiwordTags,
ColontagEnabled: zk.Config.Format.Markdown.ColonTags,
HashtagEnabled: c.Config.Format.Markdown.Hashtags,
MultiWordTagEnabled: c.Config.Format.Markdown.MultiwordTags,
ColontagEnabled: c.Config.Format.Markdown.ColonTags,
})
}
@ -127,10 +137,14 @@ func (c *Container) NoteIndexer(tx sqlite.Transaction) *sqlite.NoteIndexer {
// Database returns the DB instance for the given notebook, after executing any
// pending migration and indexing the notes if needed.
func (c *Container) Database(zk *zk.Zk, forceIndexing bool) (*sqlite.DB, note.IndexingStats, error) {
func (c *Container) Database(forceIndexing bool) (*sqlite.DB, note.IndexingStats, error) {
var stats note.IndexingStats
db, err := sqlite.Open(zk.DBPath())
if c.zkErr != nil {
return nil, stats, c.zkErr
}
db, err := sqlite.Open(c.zk.DBPath())
if err != nil {
return nil, stats, err
}
@ -139,7 +153,7 @@ func (c *Container) Database(zk *zk.Zk, forceIndexing bool) (*sqlite.DB, note.In
return nil, stats, errors.Wrap(err, "failed to migrate the database")
}
stats, err = c.index(zk, db, forceIndexing || needsReindexing)
stats, err = c.index(db, forceIndexing || needsReindexing)
if err != nil {
return nil, stats, err
}
@ -147,7 +161,7 @@ func (c *Container) Database(zk *zk.Zk, forceIndexing bool) (*sqlite.DB, note.In
return db, stats, err
}
func (c *Container) index(zk *zk.Zk, db *sqlite.DB, force bool) (note.IndexingStats, error) {
func (c *Container) index(db *sqlite.DB, force bool) (note.IndexingStats, error) {
var bar = progressbar.NewOptions(-1,
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionThrottle(100*time.Millisecond),
@ -156,11 +170,16 @@ func (c *Container) index(zk *zk.Zk, db *sqlite.DB, force bool) (note.IndexingSt
var err error
var stats note.IndexingStats
if c.zkErr != nil {
return stats, c.zkErr
}
err = db.WithTransaction(func(tx sqlite.Transaction) error {
stats, err = note.Index(
zk,
c.zk,
force,
c.Parser(zk),
c.Parser(),
c.NoteIndexer(tx),
c.Logger,
func(change paths.DiffChange) {
@ -179,8 +198,8 @@ func (c *Container) index(zk *zk.Zk, db *sqlite.DB, force bool) (note.IndexingSt
// paginated if noPager is false, using the user's pager.
//
// You can write to the pager only in the run callback.
func (c *Container) Paginate(noPager bool, config zk.Config, run func(out io.Writer) error) error {
pager, err := c.pager(noPager || config.Tool.Pager.IsEmpty(), config)
func (c *Container) Paginate(noPager bool, run func(out io.Writer) error) error {
pager, err := c.pager(noPager || c.Config.Tool.Pager.IsEmpty())
if err != nil {
return err
}
@ -189,10 +208,10 @@ func (c *Container) Paginate(noPager bool, config zk.Config, run func(out io.Wri
return err
}
func (c *Container) pager(noPager bool, config zk.Config) (*pager.Pager, error) {
func (c *Container) pager(noPager bool) (*pager.Pager, error) {
if noPager || !c.Terminal.IsInteractive() {
return pager.PassthroughPager, nil
} else {
return pager.New(config.Tool.Pager, c.Logger)
return pager.New(c.Config.Tool.Pager, c.Logger)
}
}

View File

@ -2,7 +2,6 @@ package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/mickael-menu/zk/adapter/fzf"
@ -26,17 +25,12 @@ func (cmd *Edit) Run(container *Container) error {
return err
}
wd, err := os.Getwd()
if err != nil {
return err
}
opts, err := NewFinderOpts(zk, cmd.Filtering, cmd.Sorting)
if err != nil {
return errors.Wrapf(err, "incorrect criteria")
}
db, _, err := container.Database(zk, false)
db, _, err := container.Database(false)
if err != nil {
return err
}
@ -45,10 +39,10 @@ func (cmd *Edit) Run(container *Container) error {
err = db.WithTransaction(func(tx sqlite.Transaction) error {
finder := container.NoteFinder(tx, fzf.NoteFinderOpts{
AlwaysFilter: true,
PreviewCmd: zk.Config.Tool.FzfPreview,
PreviewCmd: container.Config.Tool.FzfPreview,
NewNoteDir: cmd.newNoteDir(zk),
BasePath: zk.Path,
CurrentPath: wd,
CurrentPath: container.WorkingDir,
})
notes, err = finder.Find(*opts)
return err

View File

@ -15,12 +15,7 @@ func (cmd *Index) Help() string {
}
func (cmd *Index) Run(container *Container) error {
zk, err := container.Zk()
if err != nil {
return err
}
_, stats, err := container.Database(zk, cmd.Force)
_, stats, err := container.Database(cmd.Force)
if err != nil {
return err
}

View File

@ -39,20 +39,15 @@ func (cmd *List) Run(container *Container) error {
return err
}
db, _, err := container.Database(zk, false)
db, _, err := container.Database(false)
if err != nil {
return err
}
wd, err := os.Getwd()
if err != nil {
return err
}
templates := container.TemplateLoader(zk.Config.Note.Lang)
templates := container.TemplateLoader(container.Config.Note.Lang)
styler := container.Terminal
format := opt.NewNotEmptyString(cmd.Format)
formatter, err := note.NewFormatter(zk.Path, wd, format, templates, styler)
formatter, err := note.NewFormatter(zk.Path, container.WorkingDir, format, templates, styler)
if err != nil {
return err
}
@ -61,9 +56,9 @@ func (cmd *List) Run(container *Container) error {
err = db.WithTransaction(func(tx sqlite.Transaction) error {
finder := container.NoteFinder(tx, fzf.NoteFinderOpts{
AlwaysFilter: false,
PreviewCmd: zk.Config.Tool.FzfPreview,
PreviewCmd: container.Config.Tool.FzfPreview,
BasePath: zk.Path,
CurrentPath: wd,
CurrentPath: container.WorkingDir,
})
notes, err = finder.Find(*opts)
return err
@ -77,7 +72,7 @@ func (cmd *List) Run(container *Container) error {
count := len(notes)
if count > 0 {
err = container.Paginate(cmd.NoPager, zk.Config, func(out io.Writer) error {
err = container.Paginate(cmd.NoPager, func(out io.Writer) error {
for i, note := range notes {
if i > 0 {
fmt.Fprint(out, cmd.Delimiter)

View File

@ -12,7 +12,7 @@ import (
// New adds a new note to the notebook.
type New struct {
Directory string `arg optional type:"path" default:"." help:"Directory in which to create the note."`
Directory string `arg optional default:"." help:"Directory in which to create the note."`
Title string `short:t placeholder:TITLE help:"Title of the new note."`
Group string `short:g placeholder:NAME help:"Name of the config group this note belongs to. Takes precedence over the config of the directory."`
@ -46,7 +46,7 @@ func (cmd *New) Run(container *Container) error {
}
opts := note.CreateOpts{
Config: zk.Config,
Config: container.Config,
Dir: *dir,
Title: opt.NewNotEmptyString(cmd.Title),
Content: content,

View File

@ -9,6 +9,13 @@ import (
"github.com/mickael-menu/zk/util/paths"
)
// ErrNotebookNotFound is an error returned when a notebook cannot be found at the given path or its parents.
type ErrNotebookNotFound string
func (e ErrNotebookNotFound) Error() string {
return fmt.Sprintf("no notebook found in %s or a parent directory", string(e))
}
const defaultConfig = `# zk configuration file
#
# Uncomment the properties you want to customize.
@ -150,7 +157,7 @@ hashtags = true
#hist = "zk list --format path --delimiter0 --quiet $@ | xargs -t -0 git log --patch --"
# Edit this configuration file.
#conf = '$EDITOR "$ZK_PATH/.zk/config.toml"'
#conf = '$EDITOR "$ZK_NOTEBOOK_DIR/.zk/config.toml"'
`
const defaultTemplate = `# {{title}}
@ -164,6 +171,8 @@ type Zk struct {
Path string
// Global user configuration.
Config Config
// Working directory from which paths are relative.
workingDir string
}
// Dir represents a directory inside a notebook.
@ -177,10 +186,10 @@ type Dir struct {
}
// Open locates a notebook at the given path and parses its configuration.
func Open(path string, parentConfig Config) (*Zk, error) {
func Open(originalPath string, parentConfig Config) (*Zk, error) {
wrap := errors.Wrapper("open failed")
path, err := filepath.Abs(path)
path, err := filepath.Abs(originalPath)
if err != nil {
return nil, wrap(err)
}
@ -195,8 +204,9 @@ func Open(path string, parentConfig Config) (*Zk, error) {
}
return &Zk{
Path: path,
Config: config,
Path: path,
Config: config,
workingDir: originalPath,
}, nil
}
@ -237,7 +247,7 @@ func locateRoot(path string) (string, error) {
var locate func(string) (string, error)
locate = func(currentPath string) (string, error) {
if currentPath == "/" || currentPath == "." {
return "", fmt.Errorf("no notebook found in %v or a parent directory", path)
return "", ErrNotebookNotFound(path)
}
exists, err := paths.DirExists(filepath.Join(currentPath, ".zk"))
switch {
@ -259,19 +269,20 @@ func (zk *Zk) DBPath() string {
}
// RelPath returns the path relative to the notebook root to the given path.
func (zk *Zk) RelPath(absPath string) (string, error) {
wrap := errors.Wrapperf("%v: not a valid notebook path", absPath)
func (zk *Zk) RelPath(originalPath string) (string, error) {
wrap := errors.Wrapperf("%v: not a valid notebook path", originalPath)
path, err := filepath.Abs(absPath)
path, err := zk.absPath(originalPath)
if err != nil {
return path, wrap(err)
}
path, err = filepath.Rel(zk.Path, path)
if err != nil {
return path, wrap(err)
}
if strings.HasPrefix(path, "..") {
return path, fmt.Errorf("%s: path is outside the notebook", absPath)
return path, fmt.Errorf("%s: path is outside the notebook", originalPath)
}
if path == "." {
path = ""
@ -279,6 +290,23 @@ func (zk *Zk) RelPath(absPath string) (string, error) {
return path, nil
}
// AbsPath makes the given path absolute, using the current working directory
// as reference.
func (zk *Zk) absPath(originalPath string) (string, error) {
var err error
path := originalPath
if !filepath.IsAbs(path) {
path = filepath.Join(zk.workingDir, path)
path, err = filepath.Abs(path)
if err != nil {
return path, err
}
}
return path, nil
}
// RootDir returns the root Dir for this notebook.
func (zk *Zk) RootDir() Dir {
return Dir{
@ -290,7 +318,7 @@ func (zk *Zk) RootDir() Dir {
// DirAt returns a Dir representation of the notebook directory at the given path.
func (zk *Zk) DirAt(path string, overrides ...ConfigOverrides) (*Dir, error) {
path, err := filepath.Abs(path)
path, err := zk.absPath(path)
if err != nil {
return nil, errors.Wrapf(err, "%v: not a valid notebook directory", path)
}

View File

@ -20,10 +20,10 @@ An alias can call other aliases but cannot call itself. This enables you to over
edit = "zk edit --interactive $@"
```
When running an alias, the `ZK_PATH` environment variable is set to the absolute path of the current notebook. You can use it to run commands working no matter the location of the working directory.
When running an alias, the `ZK_NOTEBOOK_DIR` environment variable is set to the absolute path of the current notebook. You can use it to run commands working no matter the location of the working directory.
```toml
journal = 'zk new "$ZK_PATH/journal"'
journal = 'zk new "$ZK_NOTEBOOK_DIR/journal"'
```
If you need to surround the path with quotes, make sure you use double quotes, otherwise environment variables will not be expanded.
@ -70,10 +70,10 @@ recent = "zk edit --sort created- --created-after 'last two weeks' --interactive
### Edit the configuration file
Here's a concrete example using environment variables, in particular `ZK_PATH`. Note the double quotes around the path.
Here's a concrete example using environment variables, in particular `ZK_NOTEBOOK_DIR`. Note the double quotes around the path.
```toml
conf = '$EDITOR "$ZK_PATH/.zk/config.toml"'
conf = '$EDITOR "$ZK_NOTEBOOK_DIR/.zk/config.toml"'
```
### List paths in a command-line friendly fashion

View File

@ -34,12 +34,12 @@ That is a bit of a mouthful for a command called every day. Would it not be bett
```toml
[alias]
daily = 'zk new --no-input "$ZK_PATH/journal/daily"'
daily = 'zk new --no-input "$ZK_NOTEBOOK_DIR/journal/daily"'
```
Let's unpack this alias:
* `zk new` will refuse to overwrite notes. If you already created today's note, it will instead ask you if you wish to edit it. Using `--no-input` skips the prompt and edit the existing note right away.
* `$ZK_PATH` is set to the absolute path of the current [notebook](notebook.md) when running an alias. Using it allows you to run `zk daily` no matter where you are in the notebook folder hierarchy.
* We need to use double quotes around `$ZK_PATH`, otherwise it will not be expanded.
* `$ZK_NOTEBOOK_DIR` is set to the absolute path of the current [notebook](notebook.md) when running an alias. Using it allows you to run `zk daily` no matter where you are in the notebook folder hierarchy.
* We need to use double quotes around `$ZK_NOTEBOOK_DIR`, otherwise it will not be expanded.

View File

@ -4,7 +4,7 @@ A *notebook* is a directory containing a collection of notes managed by `zk`. No
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).
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.
## Anatomy of a notebook

72
main.go
View File

@ -1,10 +1,10 @@
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
"github.com/alecthomas/kong"
"github.com/mickael-menu/zk/cmd"
@ -23,7 +23,8 @@ var cli struct {
List cmd.List `cmd group:"notes" help:"List notes matching the given criteria."`
Edit cmd.Edit `cmd group:"notes" help:"Edit notes matching the given criteria."`
NoInput NoInput `help:"Never prompt or ask for confirmation."`
NoInput NoInput `help:"Never prompt or ask for confirmation."`
NotebookDir string `placeholder:"PATH" help:"Run as if zk was started in <PATH> instead of the current working directory."`
ShowHelp ShowHelp `cmd default:"1" hidden:true`
Version kong.VersionFlag `help:"Print zk version." hidden:true`
@ -57,9 +58,14 @@ func main() {
container, err := cmd.NewContainer()
fatalIfError(err)
// Open the notebook if there's any.
searchPaths, err := notebookSearchPaths()
fatalIfError(err)
container.OpenNotebook(searchPaths)
// Run the alias or command.
if isAlias, err := runAlias(container, os.Args[1:]); isAlias {
fatalIfError(err)
} else {
ctx := kong.Parse(&cli, options(container)...)
err := ctx.Run(container)
@ -112,6 +118,10 @@ func runAlias(container *cmd.Container, args []string) (bool, error) {
// Prevent infinite loop if an alias calls itself.
os.Setenv("ZK_RUNNING_ALIAS", alias)
// Move to the provided working directory if it is not the current one,
// before running the alias.
cmdStr = `cd "` + container.WorkingDir + `" && ` + cmdStr
cmd := executil.CommandFromString(cmdStr, args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
@ -130,3 +140,59 @@ func runAlias(container *cmd.Container, args []string) (bool, error) {
return false, nil
}
// notebookSearchPaths returns the places where zk will look for a notebook.
// The first successful candidate will be used as the working directory from
// which path arguments are relative from.
//
// By order of precedence:
// 1. --notebook-dir flag
// 2. current working directory
// 3. ZK_NOTEBOOK_DIR environment variable
func notebookSearchPaths() ([]string, error) {
// 1. --notebook-dir flag
notebookDir, err := parseNotebookDirFlag()
if err != nil {
return []string{}, err
}
if notebookDir != "" {
// If --notebook-dir is used, we want to only check there to report errors.
return []string{notebookDir}, nil
}
candidates := []string{}
// 2. current working directory
wd, err := os.Getwd()
if err != nil {
return nil, err
}
candidates = append(candidates, wd)
// 3. ZK_NOTEBOOK_DIR environment variable
if notebookDir, ok := os.LookupEnv("ZK_NOTEBOOK_DIR"); ok {
candidates = append(candidates, notebookDir)
}
return candidates, nil
}
// parseNotebookDir returns the path to the notebook specified with the
// --notebook-dir flag.
//
// We need to parse the --notebook-dir flag before Kong, because we might need
// it to resolve zk command aliases before parsing the CLI.
func parseNotebookDirFlag() (string, error) {
foundFlag := false
for _, arg := range os.Args {
if arg == "--notebook-dir" {
foundFlag = true
} else if foundFlag {
return arg, nil
}
}
if foundFlag {
return "", errors.New("--notebook-dir requires an argument")
}
return "", nil
}