2020-12-24 15:02:19 +00:00
package main
import (
2021-03-18 18:47:49 +00:00
"errors"
2021-01-24 11:10:13 +00:00
"fmt"
"os"
"os/exec"
2021-12-17 12:07:21 +00:00
"os/signal"
2021-04-17 09:28:38 +00:00
"path/filepath"
2022-11-05 17:57:18 +00:00
"regexp"
2021-12-17 12:07:21 +00:00
"runtime"
2021-03-18 19:27:08 +00:00
"strings"
2021-01-24 11:10:13 +00:00
2020-12-24 15:02:19 +00:00
"github.com/alecthomas/kong"
2024-01-10 21:47:22 +00:00
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/cli/cmd"
"github.com/zk-org/zk/internal/core"
executil "github.com/zk-org/zk/internal/util/exec"
2020-12-24 15:02:19 +00:00
)
2021-01-02 11:08:58 +00:00
var Version = "dev"
var Build = "dev"
2021-04-14 18:14:01 +00:00
var root struct {
2021-02-15 21:44:31 +00:00
Init cmd . Init ` cmd group:"zk" help:"Create a new notebook in the given directory." `
2021-02-10 19:53:25 +00:00
Index cmd . Index ` cmd group:"zk" help:"Index the notes to be searchable." `
2021-02-10 19:27:34 +00:00
2021-11-14 08:50:13 +00:00
New cmd . New ` cmd group:"notes" help:"Create a new note in the given notebook directory." `
List cmd . List ` cmd group:"notes" help:"List notes matching the given criteria." `
Graph cmd . Graph ` cmd group:"notes" help:"Produce a graph of the notes matching the given criteria." `
Edit cmd . Edit ` cmd group:"notes" help:"Edit notes matching the given criteria." `
Tag cmd . Tag ` cmd group:"notes" help:"Manage the note tags." `
2021-02-10 19:27:34 +00:00
2021-04-17 09:28:38 +00:00
NotebookDir string ` type:path placeholder:PATH help:"Turn off notebook auto-discovery and set manually the notebook where commands are run." `
WorkingDir string ` short:W type:path placeholder:PATH help:"Run as if zk was started in <PATH> instead of the current working directory." `
2021-03-18 18:47:49 +00:00
NoInput NoInput ` help:"Never prompt or ask for confirmation." `
2022-01-10 13:28:14 +00:00
// ForceInput is a debugging flag overriding the default value of interaction prompts.
ForceInput string ` hidden xor:"input" `
Debug bool ` default:"0" hidden help:"Print a debug stacktrace on SIGINT." `
DebugStyle bool ` default:"0" hidden help:"Force styling output as XML tags." `
2021-02-10 19:53:25 +00:00
2021-04-04 13:31:54 +00:00
ShowHelp ShowHelp ` cmd hidden default:"1" `
LSP cmd . LSP ` cmd hidden `
Version kong . VersionFlag ` hidden help:"Print zk version." `
2020-12-24 15:02:19 +00:00
}
2021-01-24 11:10:13 +00:00
// NoInput is a flag preventing any user prompt when enabled.
type NoInput bool
2021-04-14 18:14:01 +00:00
func ( f NoInput ) BeforeApply ( container * cli . Container ) error {
2021-01-24 11:10:13 +00:00
container . Terminal . NoInput = true
return nil
}
2021-02-07 18:03:27 +00:00
// ShowHelp is the default command run. It's equivalent to `zk --help`.
type ShowHelp struct { }
2021-04-14 18:14:01 +00:00
func ( cmd * ShowHelp ) Run ( container * cli . Container ) error {
parser , err := kong . New ( & root , options ( container ) ... )
2021-02-07 18:03:27 +00:00
if err != nil {
return err
}
ctx , err := parser . Parse ( [ ] string { "--help" } )
if err != nil {
return err
}
return ctx . Run ( container )
}
2020-12-24 15:02:19 +00:00
func main ( ) {
2021-08-07 16:56:26 +00:00
args := os . Args [ 1 : ]
2020-12-28 12:15:56 +00:00
// Create the dependency graph.
2021-04-14 18:14:01 +00:00
container , err := cli . NewContainer ( Version )
2021-03-17 17:04:27 +00:00
fatalIfError ( err )
2020-12-27 17:58:22 +00:00
2021-03-18 18:47:49 +00:00
// Open the notebook if there's any.
2021-08-07 16:56:26 +00:00
dirs , args , err := parseDirs ( args )
2021-03-18 18:47:49 +00:00
fatalIfError ( err )
2021-04-17 09:28:38 +00:00
searchDirs , err := notebookSearchDirs ( dirs )
fatalIfError ( err )
2021-05-16 20:11:31 +00:00
err = container . SetCurrentNotebook ( searchDirs )
fatalIfError ( err )
2021-03-18 18:47:49 +00:00
// Run the alias or command.
2021-08-07 16:56:26 +00:00
if isAlias , err := runAlias ( container , args ) ; isAlias {
2021-01-24 11:10:13 +00:00
fatalIfError ( err )
} else {
2021-08-07 16:56:26 +00:00
parser , err := kong . New ( & root , options ( container ) ... )
fatalIfError ( err )
ctx , err := parser . Parse ( args )
fatalIfError ( err )
2021-04-14 18:14:01 +00:00
2021-12-17 12:07:21 +00:00
if root . Debug {
setupDebugMode ( )
}
2022-01-10 13:28:14 +00:00
if root . DebugStyle {
container . Styler . Styler = core . TagStyler
}
container . Terminal . ForceInput = root . ForceInput
2021-12-17 12:07:21 +00:00
2021-04-14 18:14:01 +00:00
// Index the current notebook except if the user is running the `index`
// command, otherwise it would hide the stats.
if ctx . Command ( ) != "index" {
if notebook , err := container . CurrentNotebook ( ) ; err == nil {
2022-07-06 06:58:35 +00:00
index := cmd . Index { Quiet : true }
err = index . RunWithNotebook ( container , notebook )
2021-04-14 18:14:01 +00:00
ctx . FatalIfErrorf ( err )
}
}
2021-08-07 16:56:26 +00:00
err = ctx . Run ( container )
2021-01-24 11:10:13 +00:00
ctx . FatalIfErrorf ( err )
}
2020-12-24 15:02:19 +00:00
}
2021-01-23 20:15:17 +00:00
2021-04-14 18:14:01 +00:00
func options ( container * cli . Container ) [ ] kong . Option {
2021-02-10 19:27:34 +00:00
term := container . Terminal
2021-02-07 18:03:27 +00:00
return [ ] kong . Option {
kong . Bind ( container ) ,
kong . Name ( "zk" ) ,
kong . UsageOnError ( ) ,
2021-02-10 19:27:34 +00:00
kong . HelpOptions {
2021-11-14 08:50:13 +00:00
Compact : true ,
FlagsLast : true ,
WrapUpperBound : 100 ,
NoExpandSubcommands : true ,
2021-02-10 19:27:34 +00:00
} ,
2021-02-07 18:03:27 +00:00
kong . Vars {
2021-03-18 18:39:06 +00:00
"version" : "zk " + strings . TrimPrefix ( Version , "v" ) ,
2021-02-07 18:03:27 +00:00
} ,
2021-02-10 19:27:34 +00:00
kong . Groups ( map [ string ] string {
2021-10-03 16:36:59 +00:00
"cmd" : "Commands:" ,
2021-02-10 19:27:34 +00:00
"filter" : "Filtering" ,
"sort" : "Sorting" ,
"format" : "Formatting" ,
2021-04-14 18:14:01 +00:00
"notes" : term . MustStyle ( "NOTES" , core . StyleYellow , core . StyleBold ) + "\n" + term . MustStyle ( "Edit or browse your notes" , core . StyleBold ) ,
"zk" : term . MustStyle ( "NOTEBOOK" , core . StyleYellow , core . StyleBold ) + "\n" + term . MustStyle ( "A notebook is a directory containing a collection of notes" , core . StyleBold ) ,
2021-02-10 19:27:34 +00:00
} ) ,
2021-02-07 18:03:27 +00:00
}
}
2021-01-24 11:10:13 +00:00
func fatalIfError ( err error ) {
if err != nil {
fmt . Fprintf ( os . Stderr , "zk: error: %v\n" , err )
os . Exit ( 1 )
}
}
2021-01-23 20:15:17 +00:00
2021-12-17 12:07:21 +00:00
func setupDebugMode ( ) {
c := make ( chan os . Signal )
go func ( ) {
stacktrace := make ( [ ] byte , 8192 )
for _ = range c {
length := runtime . Stack ( stacktrace , true )
fmt . Fprintf ( os . Stderr , "%s\n" , string ( stacktrace [ : length ] ) )
os . Exit ( 1 )
}
} ( )
signal . Notify ( c , os . Interrupt )
}
2021-01-24 11:10:13 +00:00
// runAlias will execute a user alias if the command is one of them.
2021-04-14 18:14:01 +00:00
func runAlias ( container * cli . Container , args [ ] string ) ( bool , error ) {
2021-03-17 17:04:27 +00:00
if len ( args ) < 1 {
return false , nil
}
2021-02-13 20:07:06 +00:00
runningAlias := os . Getenv ( "ZK_RUNNING_ALIAS" )
2021-03-17 17:04:27 +00:00
for alias , cmdStr := range container . Config . Aliases {
if alias == runningAlias || alias != args [ 0 ] {
continue
}
2021-01-24 11:10:13 +00:00
2021-03-17 17:04:27 +00:00
// Prevent infinite loop if an alias calls itself.
os . Setenv ( "ZK_RUNNING_ALIAS" , alias )
2021-04-14 18:14:01 +00:00
// Move to the current notebook's root directory before running the alias.
if notebook , err := container . CurrentNotebook ( ) ; err == nil {
cmdStr = ` cd " ` + notebook . Path + ` " && ` + cmdStr
}
2021-03-18 18:47:49 +00:00
2021-03-17 17:04:27 +00:00
cmd := executil . CommandFromString ( cmdStr , args [ 1 : ] ... )
cmd . Stdin = os . Stdin
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
err := cmd . Run ( )
if err != nil {
if err , ok := err . ( * exec . ExitError ) ; ok {
os . Exit ( err . ExitCode ( ) )
return true , nil
} else {
return true , err
2021-01-24 11:10:13 +00:00
}
}
2021-03-17 17:04:27 +00:00
return true , nil
2021-01-24 11:10:13 +00:00
}
return false , nil
2021-01-23 20:15:17 +00:00
}
2021-03-18 18:47:49 +00:00
2021-04-17 09:28:38 +00:00
// notebookSearchDirs returns the places where zk will look for a notebook.
2021-03-18 18:47:49 +00:00
// The first successful candidate will be used as the working directory from
// which path arguments are relative from.
//
// By order of precedence:
2022-11-05 17:57:18 +00:00
// 1. --notebook-dir flag
// 2. current working directory
// 3. ZK_NOTEBOOK_DIR environment variable
2021-04-17 09:28:38 +00:00
func notebookSearchDirs ( dirs cli . Dirs ) ( [ ] cli . Dirs , error ) {
wd , err := os . Getwd ( )
2021-03-18 18:47:49 +00:00
if err != nil {
2021-04-17 09:28:38 +00:00
return nil , err
2021-03-18 18:47:49 +00:00
}
2021-04-17 09:28:38 +00:00
// 1. --notebook-dir flag
if dirs . NotebookDir != "" {
// If --notebook-dir is used, we want to only check there to report
// "notebook not found" errors.
if dirs . WorkingDir == "" {
dirs . WorkingDir = wd
}
return [ ] cli . Dirs { dirs } , nil
2021-03-18 18:47:49 +00:00
}
2021-04-17 09:28:38 +00:00
candidates := [ ] cli . Dirs { }
2021-03-18 18:47:49 +00:00
// 2. current working directory
2021-04-17 09:28:38 +00:00
wdDirs := dirs
if wdDirs . WorkingDir == "" {
wdDirs . WorkingDir = wd
2021-03-18 18:47:49 +00:00
}
2021-04-17 09:28:38 +00:00
wdDirs . NotebookDir = wdDirs . WorkingDir
candidates = append ( candidates , wdDirs )
2021-03-18 18:47:49 +00:00
// 3. ZK_NOTEBOOK_DIR environment variable
if notebookDir , ok := os . LookupEnv ( "ZK_NOTEBOOK_DIR" ) ; ok {
2021-04-17 09:28:38 +00:00
dirs := dirs
dirs . NotebookDir = notebookDir
if dirs . WorkingDir == "" {
dirs . WorkingDir = notebookDir
}
candidates = append ( candidates , dirs )
2021-03-18 18:47:49 +00:00
}
return candidates , nil
}
2021-04-17 09:28:38 +00:00
// parseDirs returns the paths specified with the --notebook-dir and
// --working-dir flags.
2021-03-18 18:47:49 +00:00
//
2021-04-17 09:28:38 +00:00
// We need to parse these flags before Kong, because we might need it to
// resolve zk command aliases before parsing the CLI.
2021-08-07 16:56:26 +00:00
func parseDirs ( args [ ] string ) ( cli . Dirs , [ ] string , error ) {
2021-04-17 09:28:38 +00:00
var d cli . Dirs
var err error
2022-11-05 17:57:18 +00:00
// Split str by first "=" if present and return the split pair, otherwise return nil
makeSplitPair := func ( str string ) ( pair [ ] string ) {
re := regexp . MustCompile ( ` = ` )
slice := re . FindStringIndex ( str )
if slice == nil {
return nil
}
return [ ] string { str [ : slice [ 0 ] ] , str [ slice [ 1 ] : ] }
}
// Peek ahead at next value and pair with current if it exists, otherwise return nil
makePeekPair := func ( args [ ] string , index int ) ( pair [ ] string ) {
if len ( args ) <= ( index + 1 ) {
return nil
}
return [ ] string { args [ index ] , args [ index + 1 ] }
}
matchesLongOrShort := func ( str string , long string , short string ) bool {
return str == long || ( short != "" && str == short )
}
2021-08-07 16:56:26 +00:00
findFlag := func ( long string , short string , args [ ] string ) ( string , [ ] string , error ) {
newArgs := [ ] string { }
for i , arg := range args {
2022-11-05 17:57:18 +00:00
// We can be given "--notebook-dir x" (two args) or "--notebook-dir=x" (one arg)
// so we must test against the current argument split into two, and
// the current argument + the next.
splitPair := makeSplitPair ( arg )
peekPair := makePeekPair ( args , i )
var option string
var value string
if splitPair != nil && matchesLongOrShort ( splitPair [ 0 ] , long , short ) {
option = splitPair [ 0 ]
value = splitPair [ 1 ]
// skip 1 ahead
2021-08-07 16:56:26 +00:00
newArgs = append ( newArgs , args [ i + 1 : ] ... )
2022-11-05 17:57:18 +00:00
} else if peekPair != nil && matchesLongOrShort ( peekPair [ 0 ] , long , short ) {
option = peekPair [ 0 ]
value = peekPair [ 1 ]
// skip 2 ahead (arg and value)
newArgs = append ( newArgs , args [ i + 2 : ] ... )
2021-08-07 16:56:26 +00:00
} else {
2022-11-05 17:57:18 +00:00
// we either had no split pair or peek pair, or they didn't match the
// needle, so just save the given arg and keep looking.
2021-08-07 16:56:26 +00:00
newArgs = append ( newArgs , arg )
2021-04-17 09:28:38 +00:00
}
2022-11-05 17:57:18 +00:00
if option != "" && value != "" {
path , err := filepath . Abs ( value )
return path , newArgs , err
} else if option != "" && value == "" {
return "" , newArgs , errors . New ( option + " requires a path argument" )
} else if len ( args ) == ( i + 1 ) && matchesLongOrShort ( arg , long , short ) {
return "" , newArgs , errors . New ( arg + " requires a path argument" )
}
2021-04-17 09:28:38 +00:00
}
2021-08-07 16:56:26 +00:00
return "" , newArgs , nil
2021-03-18 18:47:49 +00:00
}
2021-04-17 09:28:38 +00:00
2021-08-07 16:56:26 +00:00
d . NotebookDir , args , err = findFlag ( "--notebook-dir" , "" , args )
2021-04-17 09:28:38 +00:00
if err != nil {
2021-08-07 16:56:26 +00:00
return d , args , err
2021-04-17 09:28:38 +00:00
}
2021-08-07 16:56:26 +00:00
d . WorkingDir , args , err = findFlag ( "--working-dir" , "-W" , args )
2021-04-17 09:28:38 +00:00
if err != nil {
2021-08-07 16:56:26 +00:00
return d , args , err
2021-03-18 18:47:49 +00:00
}
2021-04-17 09:28:38 +00:00
2021-08-07 16:56:26 +00:00
return d , args , nil
2021-03-18 18:47:49 +00:00
}