yorn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/yorn +196 -0
- data/lib/entry.rb +67 -0
- data/lib/format.rb +33 -0
- data/lib/misc.rb +68 -0
- data/lib/options.rb +177 -0
- data/lib/parse.rb +88 -0
- data/lib/stdlib.rb +49 -0
- data/lib/validate.rb +21 -0
- data/lib/yornal.rb +140 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d83575804f3ca230fe0c088f91732b3aa763a378f35efc900913f1c0c422e766
|
4
|
+
data.tar.gz: 7a11a5e9a58787f209fd950cd300bacd3782a913bb7f7175b0d58617f8878bec
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3e27ef551fee30d9a74f733a4e16ca4f113ae246145ca9fee7a3d073f497dd8fea161344acb4c9ce257be14fe838d185c06f2be7053876b155ce254c6219f296
|
7
|
+
data.tar.gz: 961436d6bd24aa2c8c272303e06885ac631b06db778471636d572a506124d4531d034a7d4311ddae47ec7236de99c031d8fc647efa8857c98b8e824aefb33086
|
data/bin/yorn
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
### yorn is a program for managing journals (yornals)
|
6
|
+
### uses git for version control
|
7
|
+
|
8
|
+
require "emanlib"
|
9
|
+
require "optimist"
|
10
|
+
require "openssl"
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
13
|
+
|
14
|
+
require "stdlib"
|
15
|
+
require "misc"
|
16
|
+
require "yornal"
|
17
|
+
require "entry"
|
18
|
+
require "format"
|
19
|
+
require "parse"
|
20
|
+
require "validate"
|
21
|
+
require "options"
|
22
|
+
|
23
|
+
### globals
|
24
|
+
|
25
|
+
DEPTH = {
|
26
|
+
box: 0,
|
27
|
+
year: 1,
|
28
|
+
month: 2,
|
29
|
+
day: 3,
|
30
|
+
hour: 4,
|
31
|
+
minute: 5, min: 5,
|
32
|
+
}
|
33
|
+
|
34
|
+
SHA256 = OpenSSL::Digest.new("SHA256")
|
35
|
+
|
36
|
+
# system "rm -rf ~/.yornal.testing 2> /dev/null"
|
37
|
+
# ENV["YORNAL_PATH"] = File.expand_path "~/.yornal.testing"
|
38
|
+
|
39
|
+
data_dir = File.expand_path (ENV["XDG_DATA_HOME"] or "~/.cache") + "/yornal"
|
40
|
+
path = (ENV["YORNAL_PATH"] or data_dir or "~/.yornal")
|
41
|
+
$yornalPath = File.expand_path path
|
42
|
+
$dotyornal = [$yornalPath, ".yornal"].jomp("/")
|
43
|
+
|
44
|
+
if ARGV[0] == "git"
|
45
|
+
die do
|
46
|
+
if Dir.exist?($yornalPath)
|
47
|
+
Dir.chdir $yornalPath
|
48
|
+
system ARGV.join " "
|
49
|
+
else
|
50
|
+
err "Yornal repository does not exist."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
opts = Optimist.options do
|
56
|
+
$options.each do |option, hash|
|
57
|
+
opt option, $options[option][:syntax],
|
58
|
+
type: :string, **hash
|
59
|
+
end
|
60
|
+
|
61
|
+
opt :delete, "Delete selected entries or yornal"
|
62
|
+
opt :yes, "Assume yes when asked a question"
|
63
|
+
opt :usage, "Print example flag usage", type: :string, default: "all"
|
64
|
+
opt :full_path, "Print absolute path when printing paths", short: :F
|
65
|
+
opt :init, "Initialize yornal repository. Affected by YORNAL_PATH environment variable"
|
66
|
+
|
67
|
+
$options.each_key do |option|
|
68
|
+
[[:add], [:init], [:usage], %i[create type]]
|
69
|
+
.each do |set|
|
70
|
+
set.each { |o| conflicts o, option unless set.any? option }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
conflicts :last, :first
|
75
|
+
conflicts :print, :print_path, :delete, :edit, :view
|
76
|
+
end
|
77
|
+
|
78
|
+
$given = opts.keys
|
79
|
+
.filter { |o| o =~ /given/ }
|
80
|
+
.map { |s| s.to_s[0..(-7)].to_sym }
|
81
|
+
|
82
|
+
def given?(option)
|
83
|
+
$given.any? option
|
84
|
+
end
|
85
|
+
|
86
|
+
if $given == [:init]
|
87
|
+
err "Yornal repository already exists" if Dir.exist? [$yornalPath, ".git"].join("/")
|
88
|
+
|
89
|
+
mkdir $yornalPath
|
90
|
+
Dir.chdir $yornalPath
|
91
|
+
system "find . -type d -exec rm -rf {} 2> /dev/null"
|
92
|
+
git(:init)
|
93
|
+
File.open($dotyornal, "w") { |f| f.print "{}" }
|
94
|
+
git(:add, $dotyornal)
|
95
|
+
git(:commit, "-m 'Create yornal repository'")
|
96
|
+
die
|
97
|
+
end
|
98
|
+
|
99
|
+
if Dir.exist? $yornalPath
|
100
|
+
Dir.chdir $yornalPath
|
101
|
+
|
102
|
+
Dir.exist?(".git") or
|
103
|
+
err "Yornal repository not set up for version control. See --init"
|
104
|
+
|
105
|
+
File.exist?($dotyornal) or
|
106
|
+
err ".yornal file does not exist. Manually create it."
|
107
|
+
else
|
108
|
+
puts "Yornal repository does not exist."
|
109
|
+
puts "Use --init for repository creation."
|
110
|
+
die
|
111
|
+
end
|
112
|
+
|
113
|
+
if $given == [:usage]
|
114
|
+
die do
|
115
|
+
$options[opts[:usage].to_sym]
|
116
|
+
.when(Hash) do |hash|
|
117
|
+
puts Format.examples(opts[:usage], hash)
|
118
|
+
end
|
119
|
+
.default do
|
120
|
+
opts[:usage] == "all" or err("unrecognized flag '#{opts[:usage]}'")
|
121
|
+
$options.each do |option, hash|
|
122
|
+
Format.examples(option.to_s.gsub("_", "-"), hash)
|
123
|
+
.when(String) do |x|
|
124
|
+
puts x
|
125
|
+
puts
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
if [[:create], %i[create type]].any? { |x| x.union($given) == $given.sort }
|
133
|
+
die do
|
134
|
+
Parse.yornalType(opts[:type])
|
135
|
+
.when(Array) do |type|
|
136
|
+
Yornal.create(opts[:create], type[0])
|
137
|
+
end
|
138
|
+
.default { err "invalid type '#{opts[:type]}'" }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
$yornalName = ARGV[0]
|
143
|
+
(Yornal.report and die) if $given.empty? && !$yornalName
|
144
|
+
$yornalName or err "yornal name must be given, see --usage"
|
145
|
+
Yornal.list.keys.any? $yornalName or err "yornal '#{$yornalName}' does not exist"
|
146
|
+
$yornal = Yornal.new($yornalName)
|
147
|
+
|
148
|
+
($yornal.edit and die) if $given.empty?
|
149
|
+
|
150
|
+
if $given == [:add]
|
151
|
+
die do
|
152
|
+
add = Format.monthswap(opts[:add])
|
153
|
+
Validate.entryLiteral(add)
|
154
|
+
$yornal.edit(editor, Time.new(*add.split("/")))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
if [%i[delete yes], [:delete]].any? { |x| x.union($given) == $given.sort }
|
159
|
+
Yornal.delete($yornalName, !opts[:yes]) and die
|
160
|
+
end
|
161
|
+
|
162
|
+
$query = Format.monthswap(opts[:query])
|
163
|
+
Validate.queryFlag $query
|
164
|
+
|
165
|
+
$entries = $yornal.entries($query)
|
166
|
+
|
167
|
+
if given?(:first) || given?(:last)
|
168
|
+
method = given?(:first) ? :first : :last
|
169
|
+
type, seconds = Parse.lastFirstFlag(opts[method])
|
170
|
+
$entries = [$yornal.send(method, type, seconds, $entries)].flatten
|
171
|
+
end
|
172
|
+
|
173
|
+
$entries.filter! { |e| e.contains? opts[:match] } if given?(:match)
|
174
|
+
$entries.filter! { |e| e.matches? opts[:regex] } if given?(:regex)
|
175
|
+
|
176
|
+
die if $entries.empty?
|
177
|
+
|
178
|
+
%i[print print_path].each { |p| opts[p] = Format.special(opts[p]) }
|
179
|
+
|
180
|
+
%i[print print_path delete edit view]
|
181
|
+
.intersection($given).empty?
|
182
|
+
.when(false) do
|
183
|
+
given?(:print) and $entries.each { |e| e.printout(opts[:print]) }
|
184
|
+
given?(:print_path) and
|
185
|
+
$entries.each { |e| e.printpath(opts[:print_path], opts[:full_path]) }
|
186
|
+
given?(:delete) and $entries.each { |e| e.delete(!opts[:yes]) }
|
187
|
+
given?(:edit) and $yornal.edit(editor, Parse.editFlag(opts[:edit], $entries))
|
188
|
+
given?(:view) and $yornal.edit(pager, Parse.entrySpec(opts[:view], $entries))
|
189
|
+
die
|
190
|
+
end
|
191
|
+
|
192
|
+
if $entries.size == 1 && !given?(:query)
|
193
|
+
$entries.pop.edit
|
194
|
+
else
|
195
|
+
$entries.each { |e| e.printpath(opts[:print_path], opts[:full_path]) }
|
196
|
+
end
|
data/lib/entry.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
class Entry
|
2
|
+
include Comparable
|
3
|
+
attr_reader :date, :yornal # pseudo date, yornal (obj or name)
|
4
|
+
|
5
|
+
def <=>(other)
|
6
|
+
to_t <=> (other.is_a?(Entry) ? other.to_t : other)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.fromPath(path)
|
10
|
+
path[$yornalPath.size..].stlip("/")
|
11
|
+
.partition { |x| x =~ /^\d+$/ }
|
12
|
+
.map { |a| a.join("/") }
|
13
|
+
.then { |date, yornal| Entry.new date, yornal }
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(date, yornal)
|
17
|
+
@date = date.is_a?(Time) ? date.to_a[..5].drop_while { |i| i == 0 }.reverse.join("/") : date
|
18
|
+
@yornal = yornal.is_a?(Yornal) ? yornal : Yornal.new(yornal)
|
19
|
+
end
|
20
|
+
|
21
|
+
def path
|
22
|
+
[$yornalPath, name].jomp("/")
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
[@yornal.name, @date].jomp("/")
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_t
|
30
|
+
Time.new(*@date.split("/"))
|
31
|
+
end
|
32
|
+
|
33
|
+
def contains?(word)
|
34
|
+
File.read(path) =~ Regexp.new(word, :i)
|
35
|
+
end
|
36
|
+
|
37
|
+
def matches?(regex)
|
38
|
+
File.read(path) =~ Regexp.new(regex)
|
39
|
+
rescue RegexpError
|
40
|
+
err "Malformed regexp"
|
41
|
+
end
|
42
|
+
|
43
|
+
def edit(editor = editor(), action = :Modify, ignore = nil)
|
44
|
+
digest = SHA256.digest(File.read(path))
|
45
|
+
system "#{editor} #{path}"
|
46
|
+
return if ignore || digest == SHA256.digest(File.read(path))
|
47
|
+
|
48
|
+
git(:add, path)
|
49
|
+
git(:commit, "-m '#{action} #{@yornal.name} entry #{@date}'")
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(ask = true)
|
53
|
+
pre = "You are about to delete yornal entry '#{name}'."
|
54
|
+
question = "Are you sure you want to delete it?"
|
55
|
+
git(:rm, "#{path}") if !ask || yes_or_no?(question, pre)
|
56
|
+
end
|
57
|
+
|
58
|
+
def printout(delimiter = "\n\n")
|
59
|
+
$stdout.print File.read(path)
|
60
|
+
$stdout.print delimiter
|
61
|
+
end
|
62
|
+
|
63
|
+
def printpath(delimiter = "\n", fullpath)
|
64
|
+
$stdout.print(fullpath ? path : name)
|
65
|
+
$stdout.print delimiter
|
66
|
+
end
|
67
|
+
end
|
data/lib/format.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
class Format
|
2
|
+
def self.examples(option, hash)
|
3
|
+
return if hash[:examples].nil?
|
4
|
+
|
5
|
+
hash[:examples]
|
6
|
+
.then { |x| x.is_a?(String) ? [x] : x }
|
7
|
+
.map { |e| (" " * 2) + e }
|
8
|
+
.unshift("--#{option} examples:")
|
9
|
+
.join("\n")
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.syntax(syntax)
|
13
|
+
[syntax].flatten.join("\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.monthswap(string)
|
17
|
+
string = string.downcase
|
18
|
+
|
19
|
+
%w[ january february march april
|
20
|
+
may june july august september
|
21
|
+
october november december
|
22
|
+
].map { |m| [m, m[0..2]].map { |x| Regexp.new(x) } }
|
23
|
+
.zip(1..) do |names, i|
|
24
|
+
names.each { |n| string.gsub!(n, i.to_s) }
|
25
|
+
end
|
26
|
+
|
27
|
+
string
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.special(x)
|
31
|
+
eval "\"#{x}\""
|
32
|
+
end
|
33
|
+
end
|
data/lib/misc.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
def yes_or_no?(question, pre = nil, post = nil)
|
4
|
+
puts pre if pre
|
5
|
+
loop do
|
6
|
+
print "#{question} (yes|no): "
|
7
|
+
answer = $stdin.gets&.chomp&.downcase
|
8
|
+
die 1 if answer.nil?
|
9
|
+
if %w[yes y no n].any?(answer) || answer.empty?
|
10
|
+
puts post if post
|
11
|
+
return %w[yes y].any?(answer)
|
12
|
+
else
|
13
|
+
puts "Please enter 'yes', 'y', 'n', or 'no'."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def die(status = 0)
|
19
|
+
yield if block_given?
|
20
|
+
exit(status)
|
21
|
+
end
|
22
|
+
|
23
|
+
def err(message, *args)
|
24
|
+
$stderr.printf "Error: #{message} \n", *args
|
25
|
+
die 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def yornal_depth(dir)
|
29
|
+
return 0 unless File.directory? dir
|
30
|
+
|
31
|
+
Dir.children(dir)
|
32
|
+
.delete_if { |i| i =~ /\D/ } # remove nested yornals
|
33
|
+
.map { |i| 1 + yornal_depth([dir, i].join("/")) }.max or 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def tree(dir)
|
37
|
+
return [dir] if !(File.directory? dir) || dir =~ /\.git/
|
38
|
+
|
39
|
+
Dir.children(dir).map { |f| tree [dir, f].join("/") }.flatten
|
40
|
+
end
|
41
|
+
|
42
|
+
def defbin(x, fallback: [])
|
43
|
+
define_method(x) do
|
44
|
+
binaries = fallback
|
45
|
+
path = ENV["PATH"].split(":")
|
46
|
+
ENV[x.to_s.upcase] or
|
47
|
+
binaries.find { |b| path.any? { |p| File.exist? "#{p}/#{b}" } } or "cat"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
defbin(:editor, fallback: %w[nvim vim vi emacs zile nano code])
|
52
|
+
defbin(:pager, fallback: %w[less more])
|
53
|
+
|
54
|
+
def mkdir(path)
|
55
|
+
system "mkdir -p #{path} > /dev/null" or
|
56
|
+
err("could not create directory '%s'", path)
|
57
|
+
end
|
58
|
+
|
59
|
+
def touch(path)
|
60
|
+
spath = path.split "/"
|
61
|
+
mkdir(spath[..-2].join("/")) if spath.size > 1
|
62
|
+
File.open(path, "w") { }
|
63
|
+
end
|
64
|
+
|
65
|
+
# wrapper for git call
|
66
|
+
def git(*command)
|
67
|
+
system command.insert(0, "git").join(" ")
|
68
|
+
end
|
data/lib/options.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
$options = {
|
2
|
+
last: {
|
3
|
+
default: "1",
|
4
|
+
syntax: <<~SYNTAX,
|
5
|
+
[$n | timeSpan[±timeSpan]*]
|
6
|
+
where $j, $n ∈ NaturalNumber
|
7
|
+
and timeSpan ::= [$j.]dateAttr
|
8
|
+
and dateAttr ::= y[ear] | m[on[th]] | w[eek]
|
9
|
+
| d[ay] | h[our] | min[ute]
|
10
|
+
SYNTAX
|
11
|
+
|
12
|
+
examples: <<~EXAMPLES,
|
13
|
+
# selects the last entry in foo
|
14
|
+
yorn foo --last
|
15
|
+
# all the entries in foo in the past 3 years
|
16
|
+
yorn foo --last 3.year
|
17
|
+
# last 4 entries in bar yornal
|
18
|
+
yorn bar --last 4
|
19
|
+
# all the entries in qux in the past 2 month + 3 days
|
20
|
+
yorn qux --last 3.day+2.mon
|
21
|
+
# default action for multiple entries is to print entry paths
|
22
|
+
EXAMPLES
|
23
|
+
},
|
24
|
+
|
25
|
+
first: {
|
26
|
+
default: "1",
|
27
|
+
syntax: <<~SYNTAX,
|
28
|
+
[$n | timeSpan[±timeSpan]*]
|
29
|
+
where $j, $n ∈ NaturalNumber
|
30
|
+
and timeSpan ::= [$j.]dateAttr
|
31
|
+
and dateAttr ::= y[ear] | m[on[th]] | w[eek]
|
32
|
+
| d[ay] | h[our] | min[ute]
|
33
|
+
SYNTAX
|
34
|
+
|
35
|
+
examples: <<~EXAMPLES,
|
36
|
+
# select first 5 entries in baz
|
37
|
+
yorn baz --first 5
|
38
|
+
# select entries in the period between the first
|
39
|
+
# entry in pom and 2 months after the entry
|
40
|
+
# very similiar to --last
|
41
|
+
yorn pom --first 2.mon
|
42
|
+
EXAMPLES
|
43
|
+
},
|
44
|
+
|
45
|
+
query: {
|
46
|
+
default: "@",
|
47
|
+
syntax: <<~SYNTAX,
|
48
|
+
$year[/$month[/$day[/$hour[/$minute]]]]
|
49
|
+
where $year,$month,$day,$hour and $minute
|
50
|
+
::= int[,(int | int-int)]* | "@"
|
51
|
+
$month can be any month name as well
|
52
|
+
SYNTAX
|
53
|
+
|
54
|
+
examples: <<~EXAMPLES,
|
55
|
+
# select all entries in the 'ter' yornal (default/automatic)
|
56
|
+
yorn ter -q
|
57
|
+
# select all entries in any year where the month is august
|
58
|
+
yorn hue --query @/aug
|
59
|
+
# selects all entries even though day was specified
|
60
|
+
# because querying only cares about yornal set fields
|
61
|
+
# i.e. only the two @'s are looked at in this case
|
62
|
+
yorn monthlyJournal -q @/@/1
|
63
|
+
EXAMPLES
|
64
|
+
},
|
65
|
+
|
66
|
+
edit: {
|
67
|
+
default: "tail",
|
68
|
+
syntax: <<~SYNTAX,
|
69
|
+
loc[±$n | ±$k[±$i.dateAttr]*]
|
70
|
+
where $n, $k, $i ∈ NaturalNumber
|
71
|
+
and loc ::= [tail] | h[ead] | m[id[dle]]
|
72
|
+
and dateAttr ::= y[ear] | m[on[th]]
|
73
|
+
| w[eek] | d[ay]
|
74
|
+
| h[our] | min[ute]
|
75
|
+
SYNTAX
|
76
|
+
|
77
|
+
examples: <<~EXAMPLES,
|
78
|
+
# get entries from last year, edit the third from last entry
|
79
|
+
yorn yup -l y -e t-2
|
80
|
+
# error, as there is nothing after tail
|
81
|
+
yorn jan -e tail+1
|
82
|
+
# get all entries in hex yornal
|
83
|
+
# edit entry 1.5 months before the fourth entry
|
84
|
+
# entry won't exist, so will be created, and will be new first entry
|
85
|
+
yorn hex -e head+3-2.month+15.day
|
86
|
+
EXAMPLES
|
87
|
+
},
|
88
|
+
|
89
|
+
view: {
|
90
|
+
default: "tail",
|
91
|
+
syntax: <<~SYNTAX,
|
92
|
+
location[±$n]*
|
93
|
+
where location ::= [tail] | h[ead] | m[id[dle]]
|
94
|
+
and $n ∈ NaturalNumber
|
95
|
+
SYNTAX
|
96
|
+
|
97
|
+
examples: <<~EXAMPLES,
|
98
|
+
# view last entry in foo
|
99
|
+
yorn foo -v
|
100
|
+
# view second entry in foo
|
101
|
+
yorn foo --view head+1
|
102
|
+
# view third from last entry in foo
|
103
|
+
yorn foo -v t-1-1
|
104
|
+
EXAMPLES
|
105
|
+
},
|
106
|
+
|
107
|
+
add: {
|
108
|
+
syntax: <<~SYNTAX,
|
109
|
+
$year[/$month[/$day[/$hour[/$minute]]]]
|
110
|
+
where all ∈ NaturalNumber
|
111
|
+
and $month =~ month name
|
112
|
+
SYNTAX
|
113
|
+
|
114
|
+
examples: <<~EXAMPLES,
|
115
|
+
# adds 2022 entry to yearYornal, ignores fields not applicable
|
116
|
+
yorn yearYornal -a 2022/aug/20
|
117
|
+
EXAMPLES
|
118
|
+
},
|
119
|
+
|
120
|
+
match: {
|
121
|
+
syntax: "$word",
|
122
|
+
examples: <<~EXAMPLES,
|
123
|
+
# select entries in last 12 years
|
124
|
+
# that have the word "money" in it }
|
125
|
+
yorn foo -l 12.y -m money
|
126
|
+
# case insensitive
|
127
|
+
EXAMPLES
|
128
|
+
},
|
129
|
+
|
130
|
+
regex: {
|
131
|
+
syntax: "$regex",
|
132
|
+
examples: <<~EXAMPLES,
|
133
|
+
# select entries that have integers
|
134
|
+
yorn foo -q @ -r "^\\d+$"
|
135
|
+
EXAMPLES
|
136
|
+
},
|
137
|
+
|
138
|
+
create: {
|
139
|
+
syntax: "$yornalname",
|
140
|
+
examples: <<~EXAMPLES,
|
141
|
+
# create box yornal named foo
|
142
|
+
yorn -c foo
|
143
|
+
# create yearly yornal named foo/bar
|
144
|
+
yorn -c foo/bar -t year
|
145
|
+
# create minute yornal named qux
|
146
|
+
yorn -c qux -t min
|
147
|
+
EXAMPLES
|
148
|
+
},
|
149
|
+
|
150
|
+
type: {
|
151
|
+
default: "box",
|
152
|
+
syntax: <<~SYNTAX,
|
153
|
+
y[ear] | m[on[th]] | d[ay] |
|
154
|
+
h[our] | min[ute] | s[econd] | box
|
155
|
+
SYNTAX
|
156
|
+
},
|
157
|
+
|
158
|
+
print: {
|
159
|
+
default: '\\n\\n\\n\\n',
|
160
|
+
syntax: "$delimiter",
|
161
|
+
examples: <<~EXAMPLES,
|
162
|
+
# select all entries and print them without a delimiter
|
163
|
+
yorn foo -q @ -p ''
|
164
|
+
EXAMPLES
|
165
|
+
},
|
166
|
+
|
167
|
+
print_path: {
|
168
|
+
default: '\\n',
|
169
|
+
short: :P,
|
170
|
+
syntax: "$delimiter",
|
171
|
+
examples: <<~EXAMPLES,
|
172
|
+
# select last 3 entries and print paths"
|
173
|
+
# --print-path not needed as its default action"
|
174
|
+
yorn foo -l 3 --print-path
|
175
|
+
EXAMPLES
|
176
|
+
},
|
177
|
+
}
|
data/lib/parse.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
class Parse
|
2
|
+
@@timeFields = [
|
3
|
+
[:week, "w", "week"],
|
4
|
+
[:second, "s", "second"],
|
5
|
+
[:year, "y", "year"],
|
6
|
+
[:month, "m", "mon", "month"],
|
7
|
+
[:day, "d", "day"],
|
8
|
+
[:hour, "h", "hour"],
|
9
|
+
[:minute, "min", "minute"],
|
10
|
+
]
|
11
|
+
|
12
|
+
def self.entrySpecBase(argument, entries)
|
13
|
+
location, *operands = argument.split(/[+-]/)
|
14
|
+
ops = argument.scan(/[+-]/)
|
15
|
+
|
16
|
+
{ ["", "t", "tail"] => proc { |a, n| a[a.size - 1 + n] },
|
17
|
+
%w[h head] => proc { |a, n| a[n] },
|
18
|
+
%w[m mid middle] => proc { |a, n| a[(a.size / 2) + n] } }.find { |k, _v| k.any? location }
|
19
|
+
.default { err "undefined location '#{location}'" }
|
20
|
+
.then do |_, locator|
|
21
|
+
return locator[entries, 0].to_t if operands.empty?
|
22
|
+
|
23
|
+
yield location, operands, ops, locator
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.editFlag(argument, entries) # Time
|
28
|
+
Parse.entrySpecBase(argument, entries) do |*vars|
|
29
|
+
location, operands, ops, locator = vars
|
30
|
+
|
31
|
+
expr = operands.take_while(&:integer?).zip(ops).map(&:reverse)
|
32
|
+
ops = ops[expr.size..]
|
33
|
+
operands = operands[expr.size..]
|
34
|
+
|
35
|
+
arg = (eval expr.join) || 0
|
36
|
+
op = ops.shift
|
37
|
+
anchor = locator[entries, arg]
|
38
|
+
anchor or err("entry #{location}#{arg.to_ss} does not exist")
|
39
|
+
|
40
|
+
operands.map { |x| Parse.timeLiteral x }
|
41
|
+
.zip(ops).join
|
42
|
+
.then { |time| anchor.to_t + (op == "+" ? 1 : -1) * (eval(time) || 0) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.entrySpec(argument, entries) # Time
|
47
|
+
Parse.entrySpecBase(argument, entries) do |*vars|
|
48
|
+
_, operands, ops, locator = vars
|
49
|
+
|
50
|
+
operands.all?(&:integer?) or err("invalid argument '#{argument}'")
|
51
|
+
(operands.size == ops.size) or err("invalid expression")
|
52
|
+
arg = (eval ops.zip(operands).join) || 0
|
53
|
+
|
54
|
+
locator[entries, arg].to_t
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.lastFirstFlag(argument) # [Symbol, Integer]
|
59
|
+
return [:entry, argument.to_i] if argument.integer?
|
60
|
+
|
61
|
+
@@timeFields
|
62
|
+
.find { |_, *forms| forms.any? argument }
|
63
|
+
.when(Array) { argument = "1.#{argument}" }
|
64
|
+
|
65
|
+
operands = argument.split(/[+-]/)
|
66
|
+
ops = argument.scan(/[+-]/)
|
67
|
+
|
68
|
+
operands
|
69
|
+
.map { |x| Parse.timeLiteral x }
|
70
|
+
.zip(ops).join
|
71
|
+
.then { |r| [:time, eval(r)] }
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.timeLiteral(x) # Integer
|
75
|
+
x =~ /\d+\.[a-z]+/ or err("malformed time spec '#{x}'")
|
76
|
+
n, field = x.split(".")
|
77
|
+
|
78
|
+
@@timeFields
|
79
|
+
.find { |_m, *forms| forms.any? field }
|
80
|
+
.tap { |_| _ or err "undefined time field '#{field}'" }
|
81
|
+
.slice(0).then { |m| n.to_i.send(m) }
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.yornalType(type)
|
85
|
+
(@@timeFields[2..] + [[:box, "x", "box"]])
|
86
|
+
.find { |_, *forms| forms.any? type }
|
87
|
+
end
|
88
|
+
end
|
data/lib/stdlib.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
class Integer
|
2
|
+
def to_ss
|
3
|
+
sign = self < 0 ? "-" : "+"
|
4
|
+
"#{sign}#{abs}"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Time
|
9
|
+
def minute
|
10
|
+
min
|
11
|
+
end
|
12
|
+
|
13
|
+
def box
|
14
|
+
""
|
15
|
+
end
|
16
|
+
|
17
|
+
def path(x)
|
18
|
+
# only first 5 elements are relevant (m,h,d,mon,y)
|
19
|
+
to_a[..5].reverse.take([0, DEPTH[x] - 1].max).join("/")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class String
|
24
|
+
def lstrip_by(chars)
|
25
|
+
gsub(Regexp.new("^[#{chars}]+"), "")
|
26
|
+
end
|
27
|
+
|
28
|
+
def rstrip_by(chars)
|
29
|
+
gsub(Regexp.new("[#{chars}]+$"), "")
|
30
|
+
end
|
31
|
+
|
32
|
+
def strip_by(chars)
|
33
|
+
lstrip_by(chars).rstrip_by(chars)
|
34
|
+
end
|
35
|
+
|
36
|
+
def stlip(c)
|
37
|
+
strip_by(c).split(c)
|
38
|
+
end
|
39
|
+
|
40
|
+
def integer?
|
41
|
+
self =~ /^\d+$/
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Array
|
46
|
+
def jomp(x)
|
47
|
+
join(x).chomp(x)
|
48
|
+
end
|
49
|
+
end
|
data/lib/validate.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class Validate
|
2
|
+
def self.queryFlag(query)
|
3
|
+
query =~ %r{//} and err "consecutive / in query"
|
4
|
+
query.split("/")
|
5
|
+
.tap { |x| (x.size > 0) or err "empty query" }
|
6
|
+
.each do |n|
|
7
|
+
n.split(",").each do |n|
|
8
|
+
unless (n =~ /^\d+(-\d+)?$/) || (n == "@")
|
9
|
+
err "malformed --query component '#{n}'"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.entryLiteral(argument)
|
16
|
+
fields = argument.split("/")
|
17
|
+
fields.all?(&:integer?) or err "malformed entry literal"
|
18
|
+
(fields.size <= 6) or err "only 6 datetime fields can be specified max"
|
19
|
+
fields.map(&:to_i).all? { |i| i >= 0 } or err "negative numbers not allowed"
|
20
|
+
end
|
21
|
+
end
|
data/lib/yornal.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
class Yornal
|
2
|
+
attr_reader :name, :type
|
3
|
+
|
4
|
+
## class methods
|
5
|
+
|
6
|
+
def self.dotYornalEdit(message, &block)
|
7
|
+
File.write(
|
8
|
+
$dotyornal,
|
9
|
+
Yornal.list.tap(&block)
|
10
|
+
)
|
11
|
+
|
12
|
+
git(:add, $dotyornal)
|
13
|
+
git(:commit, "-m '#{message}'")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.create(name, type)
|
17
|
+
{ /^\./ => "begin with a dot",
|
18
|
+
%r{^/} => "begin with /",
|
19
|
+
%r{/$} => "end with /",
|
20
|
+
/\^/ => "contain ^",
|
21
|
+
%r{//} => "contain consecutive /",
|
22
|
+
/\.git/ => "contain '.git'",
|
23
|
+
/^git$/ => "be 'git'",
|
24
|
+
/^\d+$/ => "be digits only",
|
25
|
+
%r{[^/._A-za-z0-9-]} => "have chars not in [a-z], [0-9] or [/_-.]" }.each do |regex, message|
|
26
|
+
err "name cannot #{message}" if name =~ regex
|
27
|
+
end
|
28
|
+
|
29
|
+
err "yornal '#{name}' already exists" if Yornal.list.find { |n, _| n == name }
|
30
|
+
|
31
|
+
[$yornalPath, name]
|
32
|
+
.jomp("/").tap(&method(type == :box ? :touch : :mkdir))
|
33
|
+
.then { |f| git(:add, f) if type == :box }
|
34
|
+
|
35
|
+
message = "Create #{type} yornal '#{name}'"
|
36
|
+
Yornal.dotYornalEdit(message) { |h| h[name] = type }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.delete(name, ask = true)
|
40
|
+
Yornal.list.any? { |n, _| n == name } or err "'#{name}' yornal doesn't exist"
|
41
|
+
pre = "You are about to delete yornal '#{name}'."
|
42
|
+
question = "Are you sure you want to delete it?"
|
43
|
+
|
44
|
+
if !ask || yes_or_no?(question, pre)
|
45
|
+
Yornal.new(name).entries.each { |e| e.delete(ask = false) }
|
46
|
+
message = "Delete #{name} yornal"
|
47
|
+
Yornal.dotYornalEdit(message) do |h|
|
48
|
+
h.delete name
|
49
|
+
end
|
50
|
+
|
51
|
+
system "rm -rf #{name} 2> /dev/null"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.list
|
56
|
+
eval(File.read($dotyornal)).assert(Hash)
|
57
|
+
rescue Exception
|
58
|
+
err "Malformed .yornal file"
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.report
|
62
|
+
spacing = nil
|
63
|
+
countSpacing = nil
|
64
|
+
Yornal.list.keys
|
65
|
+
.map { |y| [y, Yornal.new(y).entries.size] }
|
66
|
+
.tap do |ycs|
|
67
|
+
die { puts "No yornals available. Create one with --create" } if ycs.empty?
|
68
|
+
spacing = (ycs + [["yornal"]]).map(&:first).map(&:size).max + 1
|
69
|
+
countSpacing = ycs.map(&:last).map(&:to_s).map(&:size).max
|
70
|
+
printf "%-#{spacing}s %-#{countSpacing}s type\n", "yornal", "#"
|
71
|
+
end.each do |y, c|
|
72
|
+
printf "%-#{spacing}s %-#{countSpacing}d %s\n", y, c, Yornal.new(y).type
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
## instance methods
|
77
|
+
|
78
|
+
def initialize(name)
|
79
|
+
@name = name
|
80
|
+
@type = Yornal.list[name]
|
81
|
+
end
|
82
|
+
|
83
|
+
def path
|
84
|
+
$yornalPath + "/" + @name
|
85
|
+
end
|
86
|
+
|
87
|
+
def edit(editor = editor(), time = Time.now, ignore = nil)
|
88
|
+
entryParent = [path, time.path(@type)].jomp("/")
|
89
|
+
mkdir(entryParent) unless @type == :box
|
90
|
+
entry = [entryParent, time.send(@type).to_s].jomp("/")
|
91
|
+
|
92
|
+
if File.exist? entry
|
93
|
+
Entry.fromPath(entry).edit(editor, :Modify, ignore)
|
94
|
+
else
|
95
|
+
system "touch #{entry}"
|
96
|
+
Entry.fromPath(entry).edit(editor, :Create, ignore)
|
97
|
+
File.delete entry if File.size(entry) == 0
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def entries(query = "@")
|
102
|
+
self.query(query).map { |p| Entry.fromPath p }.sort
|
103
|
+
end
|
104
|
+
|
105
|
+
# e.g. pattern: @/@/8, 2022/09/@ ; depends on yornal type (depth)
|
106
|
+
def query(pattern)
|
107
|
+
dateStructure = %i[year month day hour min]
|
108
|
+
datehash = ->(x) { x.zip(dateStructure).to_h.flip }
|
109
|
+
|
110
|
+
tree(path).filter do |path|
|
111
|
+
entry = path[$yornalPath.size + @name.size + 1..]
|
112
|
+
unless entry.split("/").join =~ /\D/ # remove nested yornals
|
113
|
+
entryHash = datehash.call(entry.stlip("/"))
|
114
|
+
patternHash = datehash.call(pattern.downcase.stlip("/"))
|
115
|
+
|
116
|
+
patternHash.map do |k, v|
|
117
|
+
!entryHash[k] or (v == "@") or
|
118
|
+
v.split(",").map do |x|
|
119
|
+
l, r = x.split("-")
|
120
|
+
(l..(r or l)) # ranges work for integer strings
|
121
|
+
end.any? { |range| range.include? entryHash[k] }
|
122
|
+
end.all? true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def first(x, n = 1, from = entries)
|
128
|
+
return from[..(n - 1)] if x == :entry
|
129
|
+
|
130
|
+
t = from[0].to_t
|
131
|
+
from.filter { |e| e < (t + n) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def last(x, n = 1, from = entries)
|
135
|
+
return from[(-n)..] if x == :entry
|
136
|
+
|
137
|
+
t = Time.now
|
138
|
+
from.filter { |e| e > (t - n) }
|
139
|
+
end
|
140
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yorn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- emanrdesu
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: optimist
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 3.2.1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 3.2.1
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: emanlib
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.0.1
|
40
|
+
description: A command-line journal management tool that uses git for version control.
|
41
|
+
email: janitor@waifu.club
|
42
|
+
executables:
|
43
|
+
- yorn
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- bin/yorn
|
48
|
+
- lib/entry.rb
|
49
|
+
- lib/format.rb
|
50
|
+
- lib/misc.rb
|
51
|
+
- lib/options.rb
|
52
|
+
- lib/parse.rb
|
53
|
+
- lib/stdlib.rb
|
54
|
+
- lib/validate.rb
|
55
|
+
- lib/yornal.rb
|
56
|
+
homepage: https://github.com/emanrdesu/yorn
|
57
|
+
licenses:
|
58
|
+
- GPL-3.0-only
|
59
|
+
metadata: {}
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 3.0.0
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.6.7
|
75
|
+
specification_version: 4
|
76
|
+
summary: A program for managing journals (yornals).
|
77
|
+
test_files: []
|