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 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: []