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.
@@ -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?
@@ -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
- 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,39 +1,50 @@
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
- # A timestamp string must be specified as the only argument. It
9
- # must exactly match to the one of the target note in the
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
- # This command starts the external editor program to edit the
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
- # 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.
22
19
 
23
20
  class Update < Command
24
- include ::Rbnotes::Utils
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. Returns the new timestamp which is associated
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
- target_stamp = Rbnotes::Utils.read_timestamp(args)
36
- editor = find_editor(conf[:editor])
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
- puts "Update the note [%s -> %s]" % [target_stamp, newstamp] unless target_stamp == newstamp
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
@@ -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,110 @@ 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
+ 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
- module_function :add_extension
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