-
Notifications
You must be signed in to change notification settings - Fork 0
/
zet
executable file
·368 lines (312 loc) · 9.6 KB
/
zet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#!/usr/bin/env bash
# /\ OSX doesn't have a >= 4.0 bash at /usr/bin.
set -o errexit
EDITOR="${EDITOR:-vi}"
ZET_DIR="${ZET_DIR:-$HOME/zet}"
BUFF_DIR="$ZET_DIR/buffer"
EXE="${EXE:="${0##*/}"}"
export ZET_DIR
export BUFF_DIR
declare -A HELP
declare reset="\e[0m"
declare red="\e[0;31m"
# TODO: assure requirements are installed.
if [[ "$OSTYPE" = "darwin"* ]]; then
find_bin="gfind" date_bin="gdate"
else
find_bin="find" date_bin="date"
fi
__git() { git -C "$ZET_DIR" "$@" ; }
__is_initialized() { [ -d "$ZET_DIR" ]; return $?; }
__err() { echo -e "${red}$@${reset}" 1>&2 ; }
__errexit() { __err "$@" ; exit 1 ; }
__check_init() {
if ! __is_initialized; then
__errexit "Zettelkasten repository has not been initialized." \
"Run 'zet init' to do so."
fi
}
__check_remote() {
local remote="${1:-origin}"
if ! __git remote show "$remote" &> /dev/null; then
__errexit "Remote '$remote' is not properly configured." \
"Run 'zet remote add \"$remote\" <URL>' to configure it."
fi
}
__buffers() {
"$find_bin" "$BUFF_DIR" -type f -name "*.md" -printf "%f\n" \
| sed -nE "s:(.*).md$:\1:pg"
}
__buff_file() { echo "$BUFF_DIR/$1.md" ; }
__buff_preview() { cat "$(__buff_file $1)" ; }
__all_zets() { "$find_bin" "$ZET_DIR" -type d -regex "$ZET_DIR/[0-9]+" -printf "%f\n" ; }
__zet_dir() { echo "$ZET_DIR/$1" ; }
__zet_file() { echo "$(__zet_dir "$1")/README.md" ; }
__zet_title() { sed -nE "s:^\s*#\s*(.*).*$:\1:p" "$(__zet_file "$1")" ; }
__zet_metadata() { sed -n "/\s*---\s*/{:loop n; /\s*---\s*/q; p; b loop}" "$(__zet_file "$1")" ; }
__zet_tags() { __zet_metadata "$1" | yq -r '.tags[]' ; }
__assure_dir() { [[ ! -d "$1" ]] && echo "Creating '$1' directory." && mkdir -p "$1" || true ; }
__assure_zet_exists() { [[ ! -e "$(__zet_file "$1")" ]] && __errexit "Zettel with ID '$1' does not exist." ; }
__zet_preview() { cat "$(__zet_file $1)" ; }
__all_tags() {
while read -r zet_id; do
__zet_tags "$zet_id"
done < <(__all_zets) \
| sort -u
exit 0
}
__all_zets_titled() {
__all_zets \
| while read zet_id; do
echo "$zet_id $(__zet_title "$zet_id")"
done
}
export -f __zet_dir
export -f __zet_file
export -f __zet_preview
export -f __buff_file
export -f __buff_preview
HELP[init.brief]="Initalizes a local repository."
HELP[init]='
Initializes a local repository.'
cmd.init() {
# TODO: if `gh` CLI is present, prompt for choosing an existent repository,
# or to create a new one.
if __is_initialized; then
echo "Zettelkasten already initialized. ($ZET_DIR)"
else
mkdir -p "$ZET_DIR"
__git init
echo "Configure a default remote with 'zet remote add origin <URL>'."
fi
}
HELP[new.brief]='Creates a new Zettel.'
HELP[new]='
Creates a new Zettel and opens a editor. The editor is configured via the
EDITOR env. variable, and should be available in the systems path. (If EDITOR is
not set, default is set to `vi`.)
A Zettel is a directory in the root of the repository named with its respective
ID. Its main file is the "README.md" (Makes it browsable on platforms like
GitHub.), but other files can be added to the repository, e.g., images etc. A
Zettel ID is generated automatically based on the instant it`s created, with the
format "YYYYmmddHHMMSSNNN".'
cmd.new() {
__check_init
local zet_id="$("$date_bin" +"%Y%m%d%H%M%S%3N")"
local zet_file="$(__zet_file "$zet_id")"
mkdir -p "$(__zet_dir "$zet_id")"
touch "$zet_file"
echo "---" >> "$zet_file"
echo "id: $zet_id" >> "$zet_file"
echo "tags: []" >> "$zet_file"
echo "---" >> "$zet_file"
# TODO: if editor exits without saving, then directory should be deleted.
exec "$EDITOR" "$zet_file"
}
HELP[buff.brief]='Accesses buffer notes.'
HELP[buff]='
Accesses a named buffer note. (Default is `inbox`.)
Buffer notes are intended to store raw information that can be latter made into
a regular Zettel. They are useful for quick notes and storage for future
projects.
Options:
-l Lists all buffers by their name.
-s Prompts the selection of a buffer for editing with a fuzzy finder.'
cmd.buff() {
__check_init
__assure_dir "$BUFF_DIR"
local buff=""
while getopts "ls" opt; do
case "$opt" in
l) __buffers ; exit 0 ;;
s) buff="$(__buffers | fzf --preview="__buff_preview {}")" ;;
# TODO: treat unknown options.
esac
done
buff="${buff:-${1:-inbox}}"
if [ -z "$buff" ]; then
__errexit "A buffer should be specified."
fi
# TODO: why the error logging when editor quits without saving?
exec "$EDITOR" "$(__buff_file "$buff")" 2> /dev/null
}
HELP[tags.brief]='Lists tags associated with a Zettel.'
HELP[tags]='
Lists the tags associated with a Zettel by its ID.
Options:
-a Lists all used tags in the repository.'
cmd.tags() {
# TODO: cache for tag research? files with indexes for each tag that can be
# used for searching, or refreshed. as of now, the performance is pretty
# bad.
__check_init
local all=0
while getopts "a" opt; do
case "$opt" in
a) all=1 ;;
esac
done
if [[ $all == 1 ]]; then
__all_tags
else
local zet_id="$1"
__assure_zet_exists "$zet_id"
__zet_tags "$zet_id"
fi
}
HELP[search.brief]='Fuzzy search in entire repository.'
HELP[search]='
Fuzzy searches for a word in all Zettels.'
cmd.search() {
__check_init
local fuzzy=0
while getopts "f" opt; do
case "$opt" in
f) fuzzy=1 ;;
esac
done
shift $((OPTIND-1))
if [[ $fuzzy == 1 ]]; then
exec rg . "$ZET_DIR" | fzf --preview='cat "$(echo {} | cut -d':' -f1)"'
else
exec rg "$@" "$ZET_DIR"
fi
}
HELP[remote.brief]='Configures remotes.'
HELP[remote]='
Enables configuration of remotes. This is a proxy to git, so it`s fully
compatible with `git remote`.
To setup a default remote: `zet remote add origin <URL>`.'
cmd.remote() {
__check_init
__git remote "$@"
}
HELP[sync.brief]='Syncs Zettels with remote repository.'
HELP[sync]='
Syncs Zettels with remote repository. (Defaul is `origin`.) It pulls updates on
remote, commits unstaged changes and pushes changes. (Note that merge may be
required, and may be performed manually in the repository`s directory.)'
cmd.sync() {
__check_init
local remote="${1:-origin}"
__check_remote "$remote"
# TODO: when conflicts happen, should a prompt for resolution be opened?
__git pull --prune "$remote" "$(__git branch --show-current)"
__git status --short || true
__git add --all || true
__git commit --verbose || true
__git push -u "$remote"
}
HELP[list.brief]="Lists Zettels."
HELP[list]='
Lists Zettels by ID and Title.'
cmd.list() {
__check_init
__all_zets_titled | sort
}
HELP[metadata.brief]='Gets metadata for a Zettel.'
HELP[metadata]='
Gets metadata for a Zettel by ID. Metadata`s data is YAML, and may be queried
using the utility `yq`.'
cmd.metadata() {
__check_init
local zet_id="$1"
__assure_zet_exists "$zet_id"
__zet_metadata "$zet_id"
}
HELP[info.brief]='Shows basic information of a zettel.'
HELP[info]='
Shows basic information of a zettel by its ID.'
cmd.info() {
__check_init
local zet_id="$1"
__assure_zet_exists "$zet_id"
echo "ID: $zet_id"
echo "File: $(__zet_file "$zet_id")"
echo "Title: \"$(__zet_title "$zet_id")\""
echo "Tags: $(__zet_metadata "$zet_id" | yq -c -r .tags)"
}
HELP[install.brief]='Install the script locally.'
HELP[install]='
Installs the script locally.
Options:
-d TARGET_DIR Target directory to install script.
-s Signs that the script should be installed with a symlink,
instead of a hard copy.'
cmd.install() {
local target_dir="/usr/local/bin"
local ln_install=false
while getopts "sd:" opt; do
case "$opt" in
d) target_dir="$OPTARG" ;;
s) ln_install=true ;;
esac
done
if [ $ln_install ]; then
echo "Installing with symbolic link."
ln -fs "$(realpath "$0")" "$target_dir/zet"
exit 0
else
cp "$0" "$target_dir"
fi
}
__help_header() {
if [ "$1" = "main" ]; then
echo "usage: $EXE <cmd>"
else
echo "usage: $EXE $1 [opts]"
fi
}
HELP[help.brief]="Shows help for a target subcommand."
HELP[help]='
Shows help for a target subcommand.'
cmd.help() {
local name="${1:-main}"
local body="${HELP[$name]}"
[[ -z "$body" ]] && __errexit "No command named '$name'"
echo "$(__help_header "$name")"
echo "$body"
}
HELP[edit.brief]="Prompts for editting an already existent Zettel."
HELP[edit]='
Prompts for editting an already existent Zettel.
Options:
-s Prompts a Zettel fuzzy selection.'
cmd.edit() {
__check_init
local selection=0
while getopts "s" opt; do
case "$opt" in
s) selection=1 ;;
esac
done
local zet_id="$1"
if [[ $selection == 1 ]]; then
zet_id="$(__all_zets_titled | fzf --preview="__zet_preview {1}" | awk '{print $1}')"
fi
__assure_zet_exists "$zet_id"
"$EDITOR" "$(__zet_file "$zet_id")"
}
while read -r line; do
[[ "$line" =~ "declare -f cmd." ]] || continue
CMDS+=( "${line##declare -f cmd.}" )
done < <(declare -F)
__cmd_summary() {
for cmd in "${CMDS[@]}"; do
echo -e "$cmd:${HELP[$cmd.brief]}";
done \
| column -t -s ":" \
| awk '{print " " $0}'
}
HELP[main]="
Zettelkasten command line interface.
Commands:
$(__cmd_summary)"
cmd="$1"
if [[ -z "$cmd" ]]; then
cmd.help
elif declare -F "cmd.$cmd" > /dev/null; then
shift; "cmd.$cmd" "$@"
else
__err "Unknown command '$cmd'" ; cmd.help ; exit 1
fi