rbnotes 0.4.2 → 0.4.7

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.
@@ -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 Commands::Search < Commands::Command
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?
@@ -34,11 +39,68 @@ module Rbnotes
34
39
  result = repo.search(pattern, timestamp_pattern)
35
40
  rescue Textrepo::InvalidSearchResultError => e
36
41
  puts e.message
37
- else
38
- result.each { |stamp, num, match|
39
- puts "#{stamp}:#{num}:#{match}"
40
- }
41
42
  end
43
+ print_search_result(result.map{ |e| SearchEntry.new(*e) })
44
+ end
45
+
46
+ def help # :nodoc:
47
+ puts <<HELP
48
+ usage:
49
+ #{Rbnotes::NAME} search PATTERN [STAMP_PATTERN]
50
+
51
+ PATTERN is a word (or words) to search, it may also be a regular
52
+ expression.
53
+
54
+ STAMP_PATTERN must be:
55
+
56
+ (a) full qualified timestamp (with suffix): "20201030160200"
57
+ (b) year and date part: "20201030"
58
+ (c) year and month part: "202010"
59
+ (d) year part only: "2020"
60
+ (e) date part only: "1030"
61
+ HELP
42
62
  end
63
+
64
+ private
65
+
66
+ # Each entry of search result is:
67
+ #
68
+ # [<timestamp>, <line_number>, <matched_text>]
69
+ #
70
+ # The sort must be done in;
71
+ #
72
+ # - descending order for <timestamp>,
73
+ # - ascending ordier for <line_number>.
74
+ #
75
+
76
+ SearchEntry = Struct.new(:timestamp, :line_number, :matched_text) {
77
+ def timestamp_size
78
+ timestamp.to_s.size
79
+ end
80
+ }
81
+
82
+ def print_search_result(entries)
83
+ maxcol_stamp = entries.map(&:timestamp_size).max
84
+ maxcol_num = entries.map(&:line_number).max
85
+
86
+ sort(entries).each { |e|
87
+ stamp_display = "%- *s" % [maxcol_stamp, e.timestamp]
88
+ num_display = "%*d" % [maxcol_num, e.line_number]
89
+
90
+ puts "#{stamp_display}: #{num_display}: #{e.matched_text}"
91
+ }
92
+ end
93
+
94
+ def sort(search_result)
95
+ search_result.sort { |a, b|
96
+ stamp_comparison = (b.timestamp <=> a.timestamp)
97
+ if stamp_comparison == 0
98
+ a.line_number <=> b.line_number
99
+ else
100
+ stamp_comparison
101
+ end
102
+ }
103
+ end
104
+
43
105
  end
44
106
  end
@@ -1,7 +1,25 @@
1
- module Rbnotes
2
- class Commands::Show < Commands::Command
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::Utils.read_timestamp(args)
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,35 +1,31 @@
1
1
  module Rbnotes::Commands
2
+
2
3
  ##
3
4
  # Updates the content of the note associated with given timestamp.
4
- # Actual modification is done interactively by the external editor.
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
12
  # When "-k" (or "--keep") option is specified, the timestamp will
9
13
  # remain unchanged.
10
14
  #
11
- # A timestamp string must be specified as the only argument. It
12
- # must exactly match to the one of the target note in the
13
- # repository. When the given timestamp was not found, the command
14
- # fails.
15
- #
16
- # Timestamp which is associated to the target note will be newly
17
- # generated with the command execution time. That is, the timestamp
18
- # before the command exection will be obsolete.
19
- #
20
- # This command starts the external editor program to edit the
21
- # content of the note. The editor program will be searched as same
22
- # as add command.
15
+ # Actual modification is done interactively by the external editor.
23
16
  #
24
- # If none of editors is available, the command fails.
17
+ # The editor program will be searched as same as add command. If
18
+ # none of editors is available, the execution fails.
25
19
 
26
20
  class Update < Command
27
- include ::Rbnotes::Utils
21
+
22
+ def description # :nodoc:
23
+ "Update the content of a note"
24
+ end
28
25
 
29
26
  ##
30
27
  # The 1st and only one argument is the timestamp to speficy the
31
- # note to update. Returns the new timestamp which is associated
32
- # to the note updated.
28
+ # note to update.
33
29
  #
34
30
  # :call-seq:
35
31
  # "20201020112233" -> "20201021123400"
@@ -47,8 +43,8 @@ module Rbnotes::Commands
47
43
  end
48
44
  end
49
45
 
50
- target_stamp = Rbnotes::Utils.read_timestamp(args)
51
- editor = find_editor(conf[:editor])
46
+ target_stamp = Rbnotes.utils.read_timestamp(args)
47
+ editor = Rbnotes.utils.find_editor(conf[:editor])
52
48
  repo = Textrepo.init(conf)
53
49
 
54
50
  text = nil
@@ -58,7 +54,7 @@ module Rbnotes::Commands
58
54
  raise Rbnotes::MissingTimestampError, target_stamp
59
55
  end
60
56
 
61
- tmpfile = run_with_tmpfile(editor, target_stamp.to_s, text)
57
+ tmpfile = Rbnotes.utils.run_with_tmpfile(editor, target_stamp.to_s, text)
62
58
  text = File.readlines(tmpfile, :chomp => true)
63
59
 
64
60
  unless text.empty?
@@ -82,5 +78,27 @@ module Rbnotes::Commands
82
78
  puts "Nothing is updated, since the specified content is empty."
83
79
  end
84
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
85
103
  end
86
104
  end
@@ -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
@@ -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
- module Utils
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,111 @@ module Rbnotes
119
121
  nil
120
122
  end
121
123
  end
122
- module_function :read_arg
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
+ timestamp_width = timestamp.to_s.size
207
+ subject_width = column - timestamp_width - delimiter.size - 1
208
+
209
+ subject = remove_heading_markup(text[0])
210
+
211
+ ts_part = "#{timestamp.to_s} "[0..(timestamp_width - 1)]
212
+ sj_part = truncate_str(subject, subject_width)
213
+
214
+ ts_part + delimiter + sj_part
215
+ end
216
+
217
+ ##
218
+ # Finds all notes those timestamps match to given patterns in the
219
+ # given repository. Returns an Array contains Timestamp objects.
220
+ #
221
+ # :call-seq:
222
+ # find_notes(Array of timestamp patterns, Textrepo::Repository)
223
+
224
+ def find_notes(timestamp_patterns, repo)
225
+ timestamp_patterns.map { |pat|
226
+ repo.entries(pat)
227
+ }.flatten.sort{ |a, b| b <=> a }.uniq
228
+ end
123
229
 
124
230
  # :stopdoc:
125
231
 
@@ -132,11 +238,70 @@ module Rbnotes
132
238
  }
133
239
  found.compact[0]
134
240
  end
135
- module_function :search_in_path
136
241
 
137
242
  def add_extension(basename)
138
243
  "#{basename}.md"
139
244
  end
140
- module_function :add_extension
245
+
246
+ def timestamp_pattern(date)
247
+ date.strftime("%Y%m%d")
248
+ end
249
+
250
+ def date_of_today
251
+ date(Time.now)
252
+ end
253
+
254
+ def date_of_yesterday
255
+ date(Time.now).prev_day
256
+ end
257
+
258
+ def date(time)
259
+ Date.new(time.year, time.mon, time.day)
260
+ end
261
+
262
+ def dates_in_this_week
263
+ dates_in_week(start_date_in_this_week)
264
+ end
265
+
266
+ def dates_in_last_week
267
+ dates_in_week(start_date_in_last_week)
268
+ end
269
+
270
+ def start_date_in_this_week
271
+ today = Time.now
272
+ Date.new(today.year, today.mon, today.day).prev_day(wday(today))
273
+ end
274
+
275
+ def start_date_in_last_week
276
+ start_date_in_this_week.prev_day(7)
277
+ end
278
+
279
+ def wday(time)
280
+ (time.wday - 1) % 7
281
+ end
282
+
283
+ def dates_in_week(start_date)
284
+ dates = [start_date]
285
+ 1.upto(6) { |i| dates << start_date.next_day(i) }
286
+ dates
287
+ end
288
+
289
+ def truncate_str(str, size)
290
+ count = 0
291
+ result = ""
292
+ str.each_char { |c|
293
+ count += Unicode::DisplayWidth.of(c)
294
+ break if count > size
295
+ result << c
296
+ }
297
+ result
298
+ end
299
+
300
+ def remove_heading_markup(str)
301
+ str.sub(/^#+ +/, '')
302
+ end
303
+
304
+ # :startdoc:
305
+
141
306
  end
142
307
  end