rbnotes 0.4.1 → 0.4.6
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 +4 -4
- data/CHANGELOG.md +41 -10
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -4
- data/README.md +4 -4
- data/conf/config.yml +1 -0
- data/conf/config_deve.yml +1 -0
- data/exe/rbnotes +10 -1
- data/lib/rbnotes.rb +8 -0
- data/lib/rbnotes/commands.rb +161 -59
- data/lib/rbnotes/commands/add.rb +43 -3
- data/lib/rbnotes/commands/delete.rb +25 -13
- data/lib/rbnotes/commands/export.rb +58 -0
- data/lib/rbnotes/commands/help.rb +98 -0
- data/lib/rbnotes/commands/import.rb +39 -2
- data/lib/rbnotes/commands/list.rb +47 -56
- data/lib/rbnotes/commands/pick.rb +47 -0
- data/lib/rbnotes/commands/search.rb +25 -2
- data/lib/rbnotes/commands/show.rb +34 -3
- data/lib/rbnotes/commands/update.rb +59 -21
- data/lib/rbnotes/error.rb +11 -0
- data/lib/rbnotes/utils.rb +174 -8
- data/lib/rbnotes/version.rb +2 -2
- data/rbnotes.gemspec +1 -1
- metadata +7 -4
@@ -0,0 +1,47 @@
|
|
1
|
+
module Rbnotes::Commands
|
2
|
+
|
3
|
+
##
|
4
|
+
# Picks a timestamp with a picker program, like `fzf`.
|
5
|
+
|
6
|
+
class Pick < Command
|
7
|
+
|
8
|
+
def description # :nodoc:
|
9
|
+
"Pick a timestamp with a picker program"
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(args, conf)
|
13
|
+
patterns = Rbnotes.utils.expand_keyword_in_args(args)
|
14
|
+
@repo = Textrepo.init(conf)
|
15
|
+
|
16
|
+
list = []
|
17
|
+
Rbnotes.utils.find_notes(patterns, @repo).each { |timestamp|
|
18
|
+
list << Rbnotes.utils.make_headline(timestamp, @repo.read(timestamp))
|
19
|
+
}
|
20
|
+
|
21
|
+
picker = conf[:picker]
|
22
|
+
unless picker.nil?
|
23
|
+
require 'open3'
|
24
|
+
result = Open3.pipeline_rw(picker) { |stdin, stdout, _|
|
25
|
+
stdin.puts list
|
26
|
+
stdin.close
|
27
|
+
stdout.read
|
28
|
+
}
|
29
|
+
puts result
|
30
|
+
else
|
31
|
+
puts list
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def help # :nodoc:
|
36
|
+
puts <<HELP
|
37
|
+
usage:
|
38
|
+
#{Rbnotes::NAME} pick
|
39
|
+
|
40
|
+
Pick a timestamp with a picker program, like `fzf`. This command
|
41
|
+
refers the configuration setting of ":picker". If no picker program
|
42
|
+
is specified, it will behave as same as "list" command.
|
43
|
+
|
44
|
+
HELP
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module Rbnotes
|
1
|
+
module Rbnotes::Commands
|
2
2
|
|
3
3
|
##
|
4
4
|
# Searches a given pattern in notes those have timestamps match a
|
@@ -22,7 +22,12 @@ module Rbnotes
|
|
22
22
|
# See the document of `Rbnotes::Commands::List#execute` to know about
|
23
23
|
# a timestamp pattern.
|
24
24
|
|
25
|
-
class
|
25
|
+
class Search < Command
|
26
|
+
|
27
|
+
def description # :nodoc:
|
28
|
+
"Search a given pattern in notes"
|
29
|
+
end
|
30
|
+
|
26
31
|
def execute(args, conf)
|
27
32
|
pattern = args.shift
|
28
33
|
raise MissingArgumentError, args if pattern.nil?
|
@@ -40,5 +45,23 @@ module Rbnotes
|
|
40
45
|
}
|
41
46
|
end
|
42
47
|
end
|
48
|
+
|
49
|
+
def help # :nodoc:
|
50
|
+
puts <<HELP
|
51
|
+
usage:
|
52
|
+
#{Rbnotes::NAME} search PATTERN [STAMP_PATTERN]
|
53
|
+
|
54
|
+
PATTERN is a word (or words) to search, it may also be a regular
|
55
|
+
expression.
|
56
|
+
|
57
|
+
STAMP_PATTERN must be:
|
58
|
+
|
59
|
+
(a) full qualified timestamp (with suffix): "20201030160200"
|
60
|
+
(b) year and date part: "20201030"
|
61
|
+
(c) year and month part: "202010"
|
62
|
+
(d) year part only: "2020"
|
63
|
+
(e) date part only: "1030"
|
64
|
+
HELP
|
65
|
+
end
|
43
66
|
end
|
44
67
|
end
|
@@ -1,7 +1,25 @@
|
|
1
|
-
module Rbnotes
|
2
|
-
|
1
|
+
module Rbnotes::Commands
|
2
|
+
|
3
|
+
##
|
4
|
+
# Shows the content of the note specified by the argument. The
|
5
|
+
# argument must be a string which can be converted into
|
6
|
+
# Textrepo::Timestamp object.
|
7
|
+
#
|
8
|
+
# A string for Timestamp must be:
|
9
|
+
#
|
10
|
+
# "20201106112600" : year, date, time and sec
|
11
|
+
# "20201106112600_012" : with suffix
|
12
|
+
#
|
13
|
+
# If no argument is passed, reads the standard input for an argument.
|
14
|
+
|
15
|
+
class Show < Command
|
16
|
+
|
17
|
+
def description # :nodoc:
|
18
|
+
"Show the content of a note"
|
19
|
+
end
|
20
|
+
|
3
21
|
def execute(args, conf)
|
4
|
-
stamp = Rbnotes
|
22
|
+
stamp = Rbnotes.utils.read_timestamp(args)
|
5
23
|
|
6
24
|
repo = Textrepo.init(conf)
|
7
25
|
content = repo.read(stamp)
|
@@ -17,5 +35,18 @@ module Rbnotes
|
|
17
35
|
puts content
|
18
36
|
end
|
19
37
|
end
|
38
|
+
|
39
|
+
def help # :nodoc:
|
40
|
+
puts <<HELP
|
41
|
+
usage:
|
42
|
+
#{Rbnotes::NAME} show [TIMESTAMP]
|
43
|
+
|
44
|
+
Show the content of given note. TIMESTAMP must be a fully qualified
|
45
|
+
one, such "20201016165130" or "20201016165130_012" if it has a suffix.
|
46
|
+
|
47
|
+
The command try to read its argument from the standard input when no
|
48
|
+
argument was passed in the command line.
|
49
|
+
HELP
|
50
|
+
end
|
20
51
|
end
|
21
52
|
end
|
@@ -1,39 +1,50 @@
|
|
1
1
|
module Rbnotes::Commands
|
2
|
+
|
2
3
|
##
|
3
4
|
# Updates the content of the note associated with given timestamp.
|
4
|
-
#
|
5
|
+
#
|
6
|
+
# Reads its argument from the standard input when no argument was
|
7
|
+
# passed in the command line.
|
8
|
+
#
|
5
9
|
# The timestamp associated with the note will be updated to new one,
|
6
10
|
# which is generated while the command exection.
|
7
11
|
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# repository. When the given timestamp was not found, the command
|
11
|
-
# fails.
|
12
|
-
#
|
13
|
-
# Timestamp which is associated to the target note will be newly
|
14
|
-
# generated with the command execution time. That is, the timestamp
|
15
|
-
# before the command exection will be obsolete.
|
12
|
+
# When "-k" (or "--keep") option is specified, the timestamp will
|
13
|
+
# remain unchanged.
|
16
14
|
#
|
17
|
-
#
|
18
|
-
# content of the note. The editor program will be searched as same
|
19
|
-
# as add command.
|
15
|
+
# Actual modification is done interactively by the external editor.
|
20
16
|
#
|
21
|
-
#
|
17
|
+
# The editor program will be searched as same as add command. If
|
18
|
+
# none of editors is available, the execution fails.
|
22
19
|
|
23
20
|
class Update < Command
|
24
|
-
|
21
|
+
|
22
|
+
def description # :nodoc:
|
23
|
+
"Update the content of a note"
|
24
|
+
end
|
25
25
|
|
26
26
|
##
|
27
27
|
# The 1st and only one argument is the timestamp to speficy the
|
28
|
-
# note to update.
|
29
|
-
# to the note updated.
|
28
|
+
# note to update.
|
30
29
|
#
|
31
30
|
# :call-seq:
|
32
31
|
# "20201020112233" -> "20201021123400"
|
33
32
|
|
34
33
|
def execute(args, conf)
|
35
|
-
|
36
|
-
|
34
|
+
@opts = {}
|
35
|
+
while args.size > 0
|
36
|
+
arg = args.shift
|
37
|
+
case arg
|
38
|
+
when "-k", "--keep"
|
39
|
+
@opts[:keep_timestamp] = true
|
40
|
+
else
|
41
|
+
args.unshift(arg)
|
42
|
+
break
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
target_stamp = Rbnotes.utils.read_timestamp(args)
|
47
|
+
editor = Rbnotes.utils.find_editor(conf[:editor])
|
37
48
|
repo = Textrepo.init(conf)
|
38
49
|
|
39
50
|
text = nil
|
@@ -43,17 +54,22 @@ module Rbnotes::Commands
|
|
43
54
|
raise Rbnotes::MissingTimestampError, target_stamp
|
44
55
|
end
|
45
56
|
|
46
|
-
tmpfile = run_with_tmpfile(editor, target_stamp.to_s, text)
|
57
|
+
tmpfile = Rbnotes.utils.run_with_tmpfile(editor, target_stamp.to_s, text)
|
47
58
|
text = File.readlines(tmpfile, :chomp => true)
|
48
59
|
|
49
60
|
unless text.empty?
|
61
|
+
keep = @opts[:keep_timestamp] || false
|
50
62
|
newstamp = nil
|
51
63
|
begin
|
52
|
-
newstamp = repo.update(target_stamp, text)
|
64
|
+
newstamp = repo.update(target_stamp, text, keep)
|
53
65
|
rescue StandardError => e
|
54
66
|
puts e.message
|
55
67
|
else
|
56
|
-
|
68
|
+
if keep
|
69
|
+
puts "Update the note content, the timestamp unchanged [%s]" % newstamp
|
70
|
+
else
|
71
|
+
puts "Update the note [%s -> %s]" % [target_stamp, newstamp] unless target_stamp == newstamp
|
72
|
+
end
|
57
73
|
ensure
|
58
74
|
# Don't forget to remove the temporary file.
|
59
75
|
File.delete(tmpfile)
|
@@ -62,5 +78,27 @@ module Rbnotes::Commands
|
|
62
78
|
puts "Nothing is updated, since the specified content is empty."
|
63
79
|
end
|
64
80
|
end
|
81
|
+
|
82
|
+
def help # :nodoc:
|
83
|
+
puts <<HELP
|
84
|
+
usage:
|
85
|
+
#{Rbnotes::NAME} update [TIMESTAMP]
|
86
|
+
|
87
|
+
Updates the content of the note associated with given timestamp.
|
88
|
+
|
89
|
+
Reads its argument from the standard input when no argument was passed
|
90
|
+
in the command line.
|
91
|
+
|
92
|
+
The timestamp associated with the note will be updated to new one,
|
93
|
+
which is generated while the command exection.
|
94
|
+
|
95
|
+
When "-k" (or "--keep") option is specified, the timestamp will remain
|
96
|
+
unchanged.
|
97
|
+
|
98
|
+
Actual modification is done interactively by the external editor. The
|
99
|
+
editor program will be searched as same as add command. If none of
|
100
|
+
editors is available, the execution fails.
|
101
|
+
HELP
|
102
|
+
end
|
65
103
|
end
|
66
104
|
end
|
data/lib/rbnotes/error.rb
CHANGED
@@ -11,6 +11,7 @@ module Rbnotes
|
|
11
11
|
MISSING_TIMESTAMP = "missing timestamp: %s"
|
12
12
|
NO_EDITOR = "No editor is available: %s"
|
13
13
|
PROGRAM_ABORT = "External program was aborted: %s"
|
14
|
+
UNKNOWN_KEYWORD = "Unknown keyword: %s"
|
14
15
|
end
|
15
16
|
|
16
17
|
# :startdoc:
|
@@ -53,4 +54,14 @@ module Rbnotes
|
|
53
54
|
super(ErrMsg::PROGRAM_ABORT % cmdline.join(" "))
|
54
55
|
end
|
55
56
|
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# An eeror raised when an unknown keyword was specified as a
|
60
|
+
# timestamp string pattern.
|
61
|
+
|
62
|
+
class UnknownKeywordError < Error
|
63
|
+
def initialize(keyword)
|
64
|
+
super(ErrMsg::UNKNOWN_KEYWORD % keyword)
|
65
|
+
end
|
66
|
+
end
|
56
67
|
end
|
data/lib/rbnotes/utils.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
|
+
require "singleton"
|
1
2
|
require "pathname"
|
2
3
|
require "tmpdir"
|
4
|
+
require "date"
|
5
|
+
require "io/console/size"
|
6
|
+
|
7
|
+
require "unicode/display_width"
|
3
8
|
|
4
9
|
module Rbnotes
|
5
10
|
##
|
6
11
|
# Defines several utility methods those are intended to be used in
|
7
12
|
# Rbnotes classes.
|
8
13
|
#
|
9
|
-
|
14
|
+
class Utils
|
15
|
+
include Singleton
|
10
16
|
|
11
17
|
##
|
12
18
|
# Finds a external editor program which is specified with the
|
@@ -26,7 +32,6 @@ module Rbnotes
|
|
26
32
|
def find_editor(preferred_editor)
|
27
33
|
find_program([preferred_editor, ENV["EDITOR"], "nano", "vi"].compact)
|
28
34
|
end
|
29
|
-
module_function :find_editor
|
30
35
|
|
31
36
|
##
|
32
37
|
# Finds a executable program in given names. When the executable
|
@@ -60,7 +65,6 @@ module Rbnotes
|
|
60
65
|
}
|
61
66
|
nil
|
62
67
|
end
|
63
|
-
module_function :find_program
|
64
68
|
|
65
69
|
##
|
66
70
|
# Executes the program with passing the given filename as argument.
|
@@ -84,7 +88,6 @@ module Rbnotes
|
|
84
88
|
raise ProgramAbortError, [prog, tmpfile] unless rc
|
85
89
|
tmpfile
|
86
90
|
end
|
87
|
-
module_function :run_with_tmpfile
|
88
91
|
|
89
92
|
##
|
90
93
|
# Generates a Textrepo::Timestamp object from a String which comes
|
@@ -98,7 +101,6 @@ module Rbnotes
|
|
98
101
|
str = args.shift || read_arg($stdin)
|
99
102
|
Textrepo::Timestamp.parse_s(str)
|
100
103
|
end
|
101
|
-
module_function :read_timestamp
|
102
104
|
|
103
105
|
##
|
104
106
|
# Reads an argument from the IO object. Typically, it is intended
|
@@ -119,7 +121,110 @@ module Rbnotes
|
|
119
121
|
nil
|
120
122
|
end
|
121
123
|
end
|
122
|
-
|
124
|
+
|
125
|
+
##
|
126
|
+
# Parses the given arguments and expand keywords if found. Each
|
127
|
+
# of the arguments is assumed to represent a timestamp pattern (or
|
128
|
+
# a keyword to be expand into several timestamp pattern). Returns
|
129
|
+
# an Array of timestamp partterns (each pattern is a String
|
130
|
+
# object).
|
131
|
+
#
|
132
|
+
# A timestamp pattern looks like:
|
133
|
+
#
|
134
|
+
# (a) full qualified timestamp (with suffix): "20201030160200"
|
135
|
+
# (b) year and date part: "20201030"
|
136
|
+
# (c) year and month part: "202010"
|
137
|
+
# (d) year part only: "2020"
|
138
|
+
# (e) date part only: "1030"
|
139
|
+
#
|
140
|
+
# KEYWORD:
|
141
|
+
#
|
142
|
+
# - "today" (or "to")
|
143
|
+
# - "yeasterday" (or "ye")
|
144
|
+
# - "this_week" (or "tw")
|
145
|
+
# - "last_week" (or "lw")
|
146
|
+
#
|
147
|
+
# :call-seq:
|
148
|
+
# expand_keyword_in_args(Array of Strings) -> Array of Strings
|
149
|
+
|
150
|
+
def expand_keyword_in_args(args)
|
151
|
+
return [nil] if args.empty?
|
152
|
+
|
153
|
+
patterns = []
|
154
|
+
while args.size > 0
|
155
|
+
arg = args.shift
|
156
|
+
if ["today", "to", "yesterday", "ye",
|
157
|
+
"this_week", "tw", "last_week", "lw"].include?(arg)
|
158
|
+
patterns.concat(Rbnotes.utils.expand_keyword(arg))
|
159
|
+
else
|
160
|
+
patterns << arg
|
161
|
+
end
|
162
|
+
end
|
163
|
+
patterns.sort.uniq
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Expands a keyword to timestamp strings.
|
168
|
+
#
|
169
|
+
# :call-seq:
|
170
|
+
# expand_keyword(keyword as String) -> Array of timestamp Strings
|
171
|
+
|
172
|
+
def expand_keyword(keyword)
|
173
|
+
patterns = []
|
174
|
+
case keyword
|
175
|
+
when "today", "to"
|
176
|
+
patterns << timestamp_pattern(date_of_today)
|
177
|
+
when "yesterday", "ye"
|
178
|
+
patterns << timestamp_pattern(date_of_yesterday)
|
179
|
+
when "this_week", "tw"
|
180
|
+
patterns.concat(dates_in_this_week.map { |d| timestamp_pattern(d) })
|
181
|
+
when "last_week", "lw"
|
182
|
+
patterns.concat(dates_in_last_week.map { |d| timestamp_pattern(d) })
|
183
|
+
else
|
184
|
+
raise UnknownKeywordError, keyword
|
185
|
+
end
|
186
|
+
patterns
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# Makes a headline with the timestamp and subject of the notes, it
|
191
|
+
# looks like as follows:
|
192
|
+
#
|
193
|
+
# |<------------------ console column size ------------------->|
|
194
|
+
# +-- timestamp ---+ +- subject (the 1st line of each note) -+
|
195
|
+
# | | | |
|
196
|
+
# 20101010001000_123: I love Macintosh. [EOL]
|
197
|
+
# 20100909090909_999: This is very very long long loooong subje[EOL]
|
198
|
+
# ++
|
199
|
+
# ^--- delimiter (2 characters)
|
200
|
+
#
|
201
|
+
# The subject part will truncate when it is long.
|
202
|
+
|
203
|
+
def make_headline(timestamp, text)
|
204
|
+
_, column = IO.console_size
|
205
|
+
delimiter = ": "
|
206
|
+
subject_width = column - TIMESTAMP_STR_MAX_WIDTH - delimiter.size - 1
|
207
|
+
|
208
|
+
subject = remove_heading_markup(text[0])
|
209
|
+
|
210
|
+
ts_part = "#{timestamp.to_s} "[0..(TIMESTAMP_STR_MAX_WIDTH - 1)]
|
211
|
+
sj_part = truncate_str(subject, subject_width)
|
212
|
+
|
213
|
+
ts_part + delimiter + sj_part
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# Finds all notes those timestamps match to given patterns in the
|
218
|
+
# given repository. Returns an Array contains Timestamp objects.
|
219
|
+
#
|
220
|
+
# :call-seq:
|
221
|
+
# find_notes(Array of timestamp patterns, Textrepo::Repository)
|
222
|
+
|
223
|
+
def find_notes(timestamp_patterns, repo)
|
224
|
+
timestamp_patterns.map { |pat|
|
225
|
+
repo.entries(pat)
|
226
|
+
}.flatten.sort{ |a, b| b <=> a }.uniq
|
227
|
+
end
|
123
228
|
|
124
229
|
# :stopdoc:
|
125
230
|
|
@@ -132,11 +237,72 @@ module Rbnotes
|
|
132
237
|
}
|
133
238
|
found.compact[0]
|
134
239
|
end
|
135
|
-
module_function :search_in_path
|
136
240
|
|
137
241
|
def add_extension(basename)
|
138
242
|
"#{basename}.md"
|
139
243
|
end
|
140
|
-
|
244
|
+
|
245
|
+
def timestamp_pattern(date)
|
246
|
+
date.strftime("%Y%m%d")
|
247
|
+
end
|
248
|
+
|
249
|
+
def date_of_today
|
250
|
+
date(Time.now)
|
251
|
+
end
|
252
|
+
|
253
|
+
def date_of_yesterday
|
254
|
+
date(Time.now).prev_day
|
255
|
+
end
|
256
|
+
|
257
|
+
def date(time)
|
258
|
+
Date.new(time.year, time.mon, time.day)
|
259
|
+
end
|
260
|
+
|
261
|
+
def dates_in_this_week
|
262
|
+
dates_in_week(start_date_in_this_week)
|
263
|
+
end
|
264
|
+
|
265
|
+
def dates_in_last_week
|
266
|
+
dates_in_week(start_date_in_last_week)
|
267
|
+
end
|
268
|
+
|
269
|
+
def start_date_in_this_week
|
270
|
+
today = Time.now
|
271
|
+
Date.new(today.year, today.mon, today.day).prev_day(wday(today))
|
272
|
+
end
|
273
|
+
|
274
|
+
def start_date_in_last_week
|
275
|
+
start_date_in_this_week.prev_day(7)
|
276
|
+
end
|
277
|
+
|
278
|
+
def wday(time)
|
279
|
+
(time.wday - 1) % 7
|
280
|
+
end
|
281
|
+
|
282
|
+
def dates_in_week(start_date)
|
283
|
+
dates = [start_date]
|
284
|
+
1.upto(6) { |i| dates << start_date.next_day(i) }
|
285
|
+
dates
|
286
|
+
end
|
287
|
+
|
288
|
+
TIMESTAMP_STR_MAX_WIDTH = "yyyymoddhhmiss_sfx".size
|
289
|
+
|
290
|
+
def truncate_str(str, size)
|
291
|
+
count = 0
|
292
|
+
result = ""
|
293
|
+
str.each_char { |c|
|
294
|
+
count += Unicode::DisplayWidth.of(c)
|
295
|
+
break if count > size
|
296
|
+
result << c
|
297
|
+
}
|
298
|
+
result
|
299
|
+
end
|
300
|
+
|
301
|
+
def remove_heading_markup(str)
|
302
|
+
str.sub(/^#+ +/, '')
|
303
|
+
end
|
304
|
+
|
305
|
+
# :startdoc:
|
306
|
+
|
141
307
|
end
|
142
308
|
end
|