rbnotes 0.4.1 → 0.4.6

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?
@@ -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