rbnotes 0.4.1 → 0.4.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|