rbnotes 0.4.2 → 0.4.7

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