You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
zk/cmd/container.go

199 lines
5.1 KiB
Go

package cmd
import (
"io"
"os"
"path/filepath"
"time"
"github.com/mickael-menu/zk/adapter/fzf"
"github.com/mickael-menu/zk/adapter/handlebars"
"github.com/mickael-menu/zk/adapter/markdown"
"github.com/mickael-menu/zk/adapter/sqlite"
"github.com/mickael-menu/zk/adapter/term"
"github.com/mickael-menu/zk/core/note"
"github.com/mickael-menu/zk/core/zk"
"github.com/mickael-menu/zk/util"
"github.com/mickael-menu/zk/util/date"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/pager"
"github.com/mickael-menu/zk/util/paths"
"github.com/schollz/progressbar/v3"
)
type Container struct {
Config zk.Config
Date date.Provider
Logger util.Logger
Terminal *term.Terminal
templateLoader *handlebars.Loader
zk *zk.Zk
zkErr error
}
func NewContainer() (*Container, error) {
wrap := errors.Wrapper("initialization")
config := zk.NewDefaultConfig()
// Load global user config
configPath, err := locateGlobalConfig()
if err != nil {
return nil, wrap(err)
}
if configPath != "" {
config, err = zk.OpenConfig(configPath, config)
if err != nil {
return nil, wrap(err)
}
}
// 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: &date,
Terminal: term.New(),
zk: zk,
zkErr: zkErr,
}, nil
}
// locateGlobalConfig looks for the global zk config file following the
// XDG Base Directory specification
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
func locateGlobalConfig() (string, error) {
configHome, ok := os.LookupEnv("XDG_CONFIG_HOME")
if !ok {
home, ok := os.LookupEnv("HOME")
if !ok {
home = "~/"
}
configHome = filepath.Join(home, ".config")
}
configPath := filepath.Join(configHome, "zk/config.toml")
exists, err := paths.Exists(configPath)
switch {
case err != nil:
return "", err
case exists:
return configPath, nil
default:
return "", nil
}
}
func (c *Container) Zk() (*zk.Zk, error) {
return c.zk, c.zkErr
}
func (c *Container) TemplateLoader(lang string) *handlebars.Loader {
if c.templateLoader == nil {
handlebars.Init(lang, c.Terminal.SupportsUTF8(), c.Logger, c.Terminal)
c.templateLoader = handlebars.NewLoader()
}
return c.templateLoader
}
func (c *Container) Parser(zk *zk.Zk) *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,
})
}
func (c *Container) NoteFinder(tx sqlite.Transaction, opts fzf.NoteFinderOpts) *fzf.NoteFinder {
notes := sqlite.NewNoteDAO(tx, c.Logger)
return fzf.NewNoteFinder(opts, notes, c.Terminal)
}
func (c *Container) NoteIndexer(tx sqlite.Transaction) *sqlite.NoteIndexer {
notes := sqlite.NewNoteDAO(tx, c.Logger)
collections := sqlite.NewCollectionDAO(tx, c.Logger)
return sqlite.NewNoteIndexer(notes, collections, c.Logger)
}
// 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) {
var stats note.IndexingStats
db, err := sqlite.Open(zk.DBPath())
if err != nil {
return nil, stats, err
}
needsReindexing, err := db.Migrate()
if err != nil {
return nil, stats, errors.Wrap(err, "failed to migrate the database")
}
stats, err = c.index(zk, db, forceIndexing || needsReindexing)
if err != nil {
return nil, stats, err
}
return db, stats, err
}
func (c *Container) index(zk *zk.Zk, db *sqlite.DB, force bool) (note.IndexingStats, error) {
var bar = progressbar.NewOptions(-1,
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionThrottle(100*time.Millisecond),
progressbar.OptionSpinnerType(14),
)
var err error
var stats note.IndexingStats
err = db.WithTransaction(func(tx sqlite.Transaction) error {
stats, err = note.Index(
zk,
force,
c.Parser(zk),
c.NoteIndexer(tx),
c.Logger,
func(change paths.DiffChange) {
bar.Add(1)
bar.Describe(change.String())
},
)
return err
})
bar.Clear()
return stats, err
}
// Paginate creates an auto-closing io.Writer which will be automatically
// 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)
if err != nil {
return err
}
err = run(pager)
pager.Close()
return err
}
func (c *Container) pager(noPager bool, config zk.Config) (*pager.Pager, error) {
if noPager || !c.Terminal.IsInteractive() {
return pager.PassthroughPager, nil
} else {
return pager.New(config.Tool.Pager, c.Logger)
}
}