rbnotes 0.4.5 → 0.4.10

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 840d2acc4c93d5b03d833b6a518c9740518e025aeb3bb2b6751864d5cf9e9d1c
4
- data.tar.gz: 759f8bdc29d64e32c73d7e02540603c3bdf84b6bdf3ac91b7a76bf8e3cf2005d
3
+ metadata.gz: 7e6de4675ffb2b409f72f240e4509a6d254793d977a8dd68cc54cf6d0c839e3d
4
+ data.tar.gz: 364d4deb47d049af7145024cfb3151639ea9cf0504963c1b72e5a15e465a84a6
5
5
  SHA512:
6
- metadata.gz: b47f361e68f7e5275b981848035621fd31342a994d6bd5d90002f91417f4806ba364714b4ea86728f993e43e74868bf08508701531361c4e785830cc772a5b2e
7
- data.tar.gz: '09cea0ff486e4e2a8521d732296e64a520c7d076088dd63c05cc4a9c219a2be28e65e50234859bab0d2772a92fb466b1ffc580525fb3565e8d68f7f7f81f8eec'
6
+ metadata.gz: bf90b255e23257a921f7a9254d4df5233f97394e73368f27f6a4a8e96989fa3aa76ab55fca69b201ecd9a15264d5d07cf974ee9b745854ca80d3c962accbe37a
7
+ data.tar.gz: c9fca12a0a6a71562a968775b09e3ebf7bda202ce7251666ab7b727236c47f8b5981d7ee703b94bfc22fae038cc161f76b10ed413ca28bcac3a9d9e0417f7f83
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
  ## [Unreleased]
8
8
  Nothing to record here.
9
9
 
10
+ ## [0.4.10] - 2020-11-20
11
+ ### Added
12
+ - Add a new command `commands` to show all command names. (#71)
13
+
14
+ ### Fixed
15
+ - Fix issue #69: crashes with invalid timestamp pattern.
16
+
17
+ ## [0.4.9] - 2020-11-17
18
+ ### Added
19
+ - Add a new option `--week` to the `list` command. (#67)
20
+
21
+ ## [0.4.8] - 2020-11-16
22
+ ### Fixed
23
+ - Fix issue #65: messy output of the `search` command.
24
+
25
+ ## [0.4.7] - 2020-11-15
26
+ ### Changed
27
+ - Beautify output of the `search` command. (#63)
28
+
29
+ ### Fixed
30
+ - Fix issue #61: `list` command fails in pipeline.
31
+
32
+ ## [0.4.6] - 2020-11-13
33
+ ### Added
34
+ - Add a new command `pick` to select a note with picker program. (#59)
35
+
10
36
  ## [0.4.5] - 2020-11-12
11
37
  ### Changed
12
38
  - Add a feature to accept multiple args for `list`. (#57)
@@ -24,7 +50,7 @@ Nothing to record here.
24
50
  - Add individual help for each command. (#42)
25
51
 
26
52
  ### Fixed
27
- - Fix `add` fails without modification (#48)
53
+ - Fix issue #48: `add` fails without modification.
28
54
 
29
55
  ## [0.4.2] - 2020-11-05
30
56
  ### Changed
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rbnotes (0.4.5)
4
+ rbnotes (0.4.10)
5
5
  textrepo (~> 0.5.4)
6
6
  unicode-display_width (~> 1.7)
7
7
 
@@ -10,7 +10,7 @@ GEM
10
10
  specs:
11
11
  minitest (5.14.2)
12
12
  rake (13.0.1)
13
- textrepo (0.5.6)
13
+ textrepo (0.5.7)
14
14
  unicode-display_width (1.7.0)
15
15
 
16
16
  PLATFORMS
@@ -4,4 +4,5 @@
4
4
  :repository_name: "notes"
5
5
  :repository_base: "~"
6
6
  :pager: "bat -l md"
7
+ :picker: "fzf"
7
8
  :editor: "/usr/local/bin/emacsclient"
@@ -4,4 +4,5 @@
4
4
  :repository_name: "notes"
5
5
  :repository_base: "tmp"
6
6
  :pager: "bat -l md"
7
+ :picker: "fzf"
7
8
  :editor: "/usr/local/bin/emacsclient"
@@ -48,9 +48,15 @@ app = App.new
48
48
  begin
49
49
  app.parse_global_options(ARGV)
50
50
  app.run(ARGV)
51
+ rescue Errno::EPIPE => e
52
+ # Fix issue #61: When the pipeline which rbnotes connects is
53
+ # discarded by the other program, the execption was raised. It does
54
+ # not end abnormally for rbnotes. So, just ignores the exception.
55
+ exit 0
51
56
  rescue MissingArgumentError, MissingTimestampError,
52
57
  NoEditorError, ProgramAbortError,
53
58
  Textrepo::InvalidTimestampStringError,
59
+ InvalidTimestampPatternError,
54
60
  ArgumentError,
55
61
  Errno::EACCES => e
56
62
  puts e.message
@@ -8,4 +8,10 @@ module Rbnotes
8
8
  require_relative "rbnotes/conf"
9
9
  require_relative "rbnotes/utils"
10
10
  require_relative "rbnotes/commands"
11
+
12
+ class << self
13
+ def utils
14
+ Utils.instance
15
+ end
16
+ end
11
17
  end
@@ -58,13 +58,14 @@ Syntax:
58
58
 
59
59
  Example usage:
60
60
  #{Rbnotes::NAME} add [-t STAMP_PATTERN]
61
+ #{Rbnotes::NAME} commands [-d]
61
62
  #{Rbnotes::NAME} delete [TIMESTAMP]
62
63
  #{Rbnotes::NAME} export [TIMESTAMP [FILENAME]]
63
64
  #{Rbnotes::NAME} import FILE
64
65
  #{Rbnotes::NAME} list [STAMP_PATTERN|KEYWORD]
65
66
  #{Rbnotes::NAME} search PATTERN [STAMP_PATTERN]
66
67
  #{Rbnotes::NAME} show [TIMESTAMP]
67
- #{Rbnotes::NAME} update [TIMESTAMP]
68
+ #{Rbnotes::NAME} update [-k] [TIMESTAMP]
68
69
 
69
70
  Further help for each command:
70
71
  #{Rbnotes::NAME} help commands
@@ -27,7 +27,6 @@ module Rbnotes::Commands
27
27
  # If none of the above editor is available, the command fails.
28
28
 
29
29
  class Add < Command
30
- include ::Rbnotes::Utils
31
30
 
32
31
  def description # :nodoc:
33
32
  "Add a new note"
@@ -52,10 +51,10 @@ module Rbnotes::Commands
52
51
  stamp = @opts[:timestamp] || Textrepo::Timestamp.new(Time.now)
53
52
 
54
53
  candidates = [conf[:editor], ENV["EDITOR"], "nano", "vi"].compact
55
- editor = find_program(candidates)
54
+ editor = Rbnotes.utils.find_program(candidates)
56
55
  raise Rbnotes::NoEditorError, candidates if editor.nil?
57
56
 
58
- tmpfile = run_with_tmpfile(editor, stamp.to_s)
57
+ tmpfile = Rbnotes.utils.run_with_tmpfile(editor, stamp.to_s)
59
58
 
60
59
  unless FileTest.exist?(tmpfile)
61
60
  puts "Cancel adding, since nothing to store"
@@ -0,0 +1,121 @@
1
+ module Rbnotes::Commands
2
+ ##
3
+ # Prints all command names into a single line. When `-d` (or
4
+ # `--deve-commands`) was specified, development commands (such
5
+ # `conf`) would be also printed in addition to general commands.
6
+
7
+ class Commands < Command
8
+
9
+ def description # :nodoc:
10
+ "Print all command names into a single line"
11
+ end
12
+
13
+ def execute(args, conf)
14
+ @opts = {}
15
+ while args.size > 0
16
+ arg = args.shift
17
+ case arg.to_s
18
+ when "" # no options
19
+ break
20
+ when "-d", "--deve-commands"
21
+ @opts[:print_deve_commands] = true
22
+ else # invalid options or args
23
+ args.unshift(arg)
24
+ raise ArgumentError, "invalid option or argument: %s" % args.join(" ")
25
+ end
26
+ end
27
+
28
+ puts commands(@opts[:print_deve_commands]).join(" ")
29
+ end
30
+
31
+ def help
32
+ puts <<HELP
33
+ usage:
34
+ #{Rbnotes::NAME} [-d|--deve-commands]
35
+
36
+ Print all command names into a single line. If "-d" option (or
37
+ "--deve-commands") is specified, commands for development purpose are
38
+ also printed.
39
+
40
+ HELP
41
+ print_commands
42
+ end
43
+
44
+ # :stopdoc:
45
+ private
46
+
47
+ ##
48
+ # Enumerates all command names.
49
+ #
50
+ # :call-seq:
51
+ # commands(builtins = false) -> [Array of Strings]
52
+
53
+ def commands(include_builtins = false)
54
+ names = external_commands.map { |cmd| cmd.to_s.downcase }
55
+ names += builtin_commands.map { |cmd| cmd.to_s.downcase } if include_builtins
56
+ names
57
+ end
58
+
59
+ def external_commands
60
+ Dir.glob("*.rb", :base => __dir__) { |rb|
61
+ require_relative rb
62
+ }
63
+ Rbnotes::Commands.constants.difference([:Builtins, :Command]).sort
64
+ end
65
+
66
+ def builtin_commands
67
+ Rbnotes::Commands::Builtins.constants.sort
68
+ end
69
+
70
+ def print_commands
71
+ Dir.glob("*.rb", :base => __dir__) { |rb|
72
+ require_relative rb
73
+ }
74
+ puts "#{Rbnotes::NAME.capitalize} Commands:"
75
+ print_commands_desc(external_commands)
76
+ puts
77
+ puts "for development purpose"
78
+ print_builtins_desc(builtin_commands)
79
+ end
80
+
81
+ def print_commands_desc(commands)
82
+ print_desc(Rbnotes::Commands, commands)
83
+ end
84
+
85
+ def print_builtins_desc(builtins)
86
+ print_desc(Rbnotes::Commands::Builtins, builtins)
87
+ end
88
+
89
+ class CmdNames
90
+ attr_reader :symbol, :name, :size
91
+ def initialize(cmd)
92
+ @symbol = cmd
93
+ @name = cmd.to_s.downcase
94
+ @size = name.size
95
+ end
96
+ end
97
+
98
+ def print_desc(mod, commands)
99
+ cmds = commands.map { |cmd| CmdNames.new(cmd) }
100
+ name_part_size = cmds.map(&:size).max + 2
101
+ cmds.map { |cmd|
102
+ puts "#{spaces(4)}#{name_part(cmd.name, name_part_size)} #{desc_part(cmd.symbol, mod)}"
103
+ }
104
+ end
105
+
106
+ def name_part(name, size)
107
+ "#{name}#{spaces(size)}"[0, size]
108
+ end
109
+
110
+ def desc_part(symbol, mod)
111
+ mod.const_get(symbol, false).new.description
112
+ end
113
+
114
+ def spaces(size)
115
+ Array.new(size, " ").join
116
+ end
117
+
118
+ # :startdoc:
119
+
120
+ end
121
+ end
@@ -13,7 +13,7 @@ module Rbnotes::Commands
13
13
  end
14
14
 
15
15
  def execute(args, conf)
16
- stamp = Rbnotes::Utils.read_timestamp(args)
16
+ stamp = Rbnotes.utils.read_timestamp(args)
17
17
 
18
18
  repo = Textrepo.init(conf)
19
19
  begin
@@ -21,7 +21,7 @@ module Rbnotes::Commands
21
21
  # execute([a String as timestring], Rbnotes::Conf or Hash) -> nil
22
22
 
23
23
  def execute(args, conf)
24
- stamp = Rbnotes::Utils.read_timestamp(args)
24
+ stamp = Rbnotes.utils.read_timestamp(args)
25
25
 
26
26
  repo = Textrepo.init(conf)
27
27
  begin
@@ -7,7 +7,7 @@ module Rbnotes::Commands
7
7
  class Help < Command
8
8
 
9
9
  def description # :nodoc:
10
- "Provide help on each command"
10
+ "Print help of each command"
11
11
  end
12
12
 
13
13
  ##
@@ -20,8 +20,6 @@ module Rbnotes::Commands
20
20
  case cmd_name
21
21
  when nil
22
22
  self.help
23
- when "commands"
24
- print_commands
25
23
  else
26
24
  Commands.load(cmd_name).help
27
25
  end
@@ -59,40 +57,5 @@ Further information:
59
57
  HELP
60
58
  end
61
59
 
62
- # :stopdoc:
63
- private
64
-
65
- def print_commands
66
- Dir.glob("*.rb", :base => __dir__) { |rb|
67
- next if rb == "help.rb"
68
- require_relative rb
69
- }
70
- commands = Commands.constants.difference([:Builtins, :Command])
71
- builtins = Commands::Builtins.constants
72
-
73
- puts "#{Rbnotes::NAME.capitalize} Commands:"
74
- print_commands_desc(commands.sort)
75
- puts
76
- puts "for development purpose"
77
- print_builtins_desc(builtins.sort)
78
- end
79
-
80
- def print_commands_desc(commands)
81
- print_desc(Commands, commands)
82
- end
83
-
84
- def print_builtins_desc(builtins)
85
- print_desc(Commands::Builtins, builtins)
86
- end
87
-
88
- def print_desc(mod, commands)
89
- commands.map { |cmd|
90
- name = "#{cmd.to_s.downcase} "[0, 8]
91
- desc = mod.const_get(cmd, false).new.description
92
- puts " #{name} #{desc}"
93
- }
94
- end
95
-
96
- # :startdoc:
97
60
  end
98
61
  end
@@ -1,7 +1,3 @@
1
- require "date"
2
- require "unicode/display_width"
3
- require "io/console/size"
4
-
5
1
  module Rbnotes::Commands
6
2
 
7
3
  ##
@@ -55,22 +51,45 @@ module Rbnotes::Commands
55
51
  # execute(Array, Rbnotes::Conf or Hash) -> nil
56
52
 
57
53
  def execute(args, conf)
58
- patterns = args.size > 0 ? convert_keyword(args) : [nil]
54
+ @opts = {}
55
+ while args.size > 0
56
+ arg = args.shift
57
+ case arg
58
+ when "-w", "--week"
59
+ @opts[:enum_week] = true
60
+ else
61
+ args.unshift(arg)
62
+ break
63
+ end
64
+ end
65
+
66
+ patterns = nil
67
+ if @opts[:enum_week]
68
+ arg = args.shift || Textrepo::Timestamp.now[0, 8]
69
+ case arg.size
70
+ when "yyyymodd".size, "yyyymoddhhmiss".size, "yyyymoddhhmiss_sfx".size
71
+ stamp_str = "#{arg}000000"[0, 14]
72
+ timestamp = Textrepo::Timestamp.parse_s(stamp_str)
73
+ patterns = Rbnotes.utils.timestamp_patterns_in_week(timestamp)
74
+ else
75
+ raise InvalidTimestampPatternError,
76
+ "cannot convert to a date [%s]" % args.unshift(arg)
77
+ end
78
+ else
79
+ patterns = Rbnotes.utils.expand_keyword_in_args(args)
80
+ end
59
81
 
60
82
  @repo = Textrepo.init(conf)
61
83
  # newer stamp shoud be above
62
- stamps = patterns.map { |pat|
63
- @repo.entries(pat)
64
- }.flatten.sort{|a, b| b <=> a}.uniq
65
- stamps.each { |timestamp|
66
- puts make_headline(timestamp)
84
+ Rbnotes.utils.find_notes(patterns, @repo).each { |timestamp|
85
+ puts Rbnotes.utils.make_headline(timestamp, @repo.read(timestamp))
67
86
  }
68
87
  end
69
88
 
70
89
  def help # :nodoc:
71
90
  puts <<HELP
72
91
  usage:
73
- #{Rbnotes::NAME} list [STAMP_PATTERN|KEYWORD]
92
+ #{Rbnotes::NAME} list [-w|--week][STAMP_PATTERN|KEYWORD]
74
93
 
75
94
  Show a list of notes. When no arguments, make a list with all notes
76
95
  in the repository. When specified STAMP_PATTERN, only those match the
@@ -92,100 +111,18 @@ KEYWORD:
92
111
  - "this_week" (or "tw")
93
112
  - "last_week" (or "lw")
94
113
 
95
- HELP
96
- end
97
-
98
- # :stopdoc:
99
-
100
- private
101
- TIMESTAMP_STR_MAX_WIDTH = "yyyymoddhhmiss_sfx".size
102
-
103
- ##
104
- # Makes a headline with the timestamp and subject of the notes, it
105
- # looks like as follows:
106
- #
107
- # |<------------------ console column size ------------------->|
108
- # +-- timestamp ---+ +- subject (the 1st line of each note) -+
109
- # | | | |
110
- # 20101010001000_123: I love Macintosh. [EOL]
111
- # 20100909090909_999: This is very very long long loooong subje[EOL]
112
- # ++
113
- # ^--- delimiter (2 characters)
114
- #
115
- # The subject part will truncate when it is long.
116
-
117
- def make_headline(timestamp)
118
- _, column = IO.console_size
119
- delimiter = ": "
120
- subject_width = column - TIMESTAMP_STR_MAX_WIDTH - delimiter.size - 1
114
+ An option "--week" is also acceptable. It specifies to enumerate all
115
+ days of a week. Typically, the option is used with a STAMP_PATTERN
116
+ which specifies a date, such "20201117", then it enumerates all days
117
+ of the week which contains "17th November 2020".
121
118
 
122
- subject = remove_heading_markup(@repo.read(timestamp)[0])
119
+ A STAMP_PATTERN other than (a) and (b) causes an error if it was used
120
+ with "--week" option.
123
121
 
124
- ts_part = "#{timestamp.to_s} "[0..(TIMESTAMP_STR_MAX_WIDTH - 1)]
125
- sj_part = truncate_str(subject, subject_width)
126
-
127
- ts_part + delimiter + sj_part
128
- end
129
-
130
- def truncate_str(str, size)
131
- count = 0
132
- result = ""
133
- str.each_char { |c|
134
- count += Unicode::DisplayWidth.of(c)
135
- break if count > size
136
- result << c
137
- }
138
- result
139
- end
140
-
141
- def remove_heading_markup(str)
142
- str.sub(/^#+ +/, '')
143
- end
144
-
145
- def convert_keyword(args)
146
- patterns = []
147
- while args.size > 0
148
- arg = args.shift
149
- case arg.to_s
150
- when "today", "to"
151
- patterns << Textrepo::Timestamp.new(Time.now).to_s[0..7]
152
- when "yesterday", "ye"
153
- t = Time.now
154
- patterns << Date.new(t.year, t.mon, t.day).prev_day.strftime("%Y%m%d")
155
- when "this_week", "tw"
156
- patterns.concat(dates_in_this_week)
157
- when "last_week", "lw"
158
- patterns.concat(dates_in_last_week)
159
- else
160
- patterns << arg
161
- end
162
- end
163
- patterns.sort!.uniq
164
- end
165
-
166
- # week day for Monday start calendar
167
- def wday(time)
168
- (time.wday - 1) % 7
169
- end
170
-
171
- def dates_in_this_week
172
- to = Time.now
173
- start = Date.new(to.year, to.mon, to.day).prev_day(wday(to))
174
- dates_in_week(start)
175
- end
176
-
177
- def dates_in_last_week
178
- to = Time.now
179
- start_of_this_week = Date.new(to.year, to.mon, to.day).prev_day(wday(to))
180
- dates_in_week(start_of_this_week.prev_day(7))
181
- end
182
-
183
- def dates_in_week(start_date)
184
- dates = [start_date]
185
- 1.upto(6) { |i| dates << start_date.next_day(i) }
186
- dates.map { |d| d.strftime("%Y%m%d") }
122
+ When no STAMP_PATTERN was specified with "--week" option, the output
123
+ would be as same as the KEYWORD, "this_week" was specified.
124
+ HELP
187
125
  end
188
126
 
189
- # :startdoc:
190
127
  end
191
128
  end
@@ -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
@@ -39,11 +39,8 @@ module Rbnotes::Commands
39
39
  result = repo.search(pattern, timestamp_pattern)
40
40
  rescue Textrepo::InvalidSearchResultError => e
41
41
  puts e.message
42
- else
43
- result.each { |stamp, num, match|
44
- puts "#{stamp}:#{num}:#{match}"
45
- }
46
42
  end
43
+ print_search_result(result.map{ |e| SearchEntry.new(*e) })
47
44
  end
48
45
 
49
46
  def help # :nodoc:
@@ -63,5 +60,51 @@ STAMP_PATTERN must be:
63
60
  (e) date part only: "1030"
64
61
  HELP
65
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
+ def line_number_digits_size
82
+ line_number.to_s.size
83
+ end
84
+ }
85
+
86
+ def print_search_result(entries)
87
+ maxcol_stamp = entries.map(&:timestamp_size).max
88
+ maxcol_num = entries.map(&:line_number_digits_size).max
89
+
90
+ sort(entries).each { |e|
91
+ stamp_display = "%- *s" % [maxcol_stamp, e.timestamp]
92
+ num_display = "%*d" % [maxcol_num, e.line_number]
93
+
94
+ puts "#{stamp_display}: #{num_display}: #{e.matched_text}"
95
+ }
96
+ end
97
+
98
+ def sort(search_result)
99
+ search_result.sort { |a, b|
100
+ stamp_comparison = (b.timestamp <=> a.timestamp)
101
+ if stamp_comparison == 0
102
+ a.line_number <=> b.line_number
103
+ else
104
+ stamp_comparison
105
+ end
106
+ }
107
+ end
108
+
66
109
  end
67
110
  end
@@ -19,7 +19,7 @@ module Rbnotes::Commands
19
19
  end
20
20
 
21
21
  def execute(args, conf)
22
- stamp = Rbnotes::Utils.read_timestamp(args)
22
+ stamp = Rbnotes.utils.read_timestamp(args)
23
23
 
24
24
  repo = Textrepo.init(conf)
25
25
  content = repo.read(stamp)
@@ -18,7 +18,6 @@ module Rbnotes::Commands
18
18
  # none of editors is available, the execution fails.
19
19
 
20
20
  class Update < Command
21
- include ::Rbnotes::Utils
22
21
 
23
22
  def description # :nodoc:
24
23
  "Update the content of a note"
@@ -44,8 +43,8 @@ module Rbnotes::Commands
44
43
  end
45
44
  end
46
45
 
47
- target_stamp = Rbnotes::Utils.read_timestamp(args)
48
- editor = find_editor(conf[:editor])
46
+ target_stamp = Rbnotes.utils.read_timestamp(args)
47
+ editor = Rbnotes.utils.find_editor(conf[:editor])
49
48
  repo = Textrepo.init(conf)
50
49
 
51
50
  text = nil
@@ -55,7 +54,7 @@ module Rbnotes::Commands
55
54
  raise Rbnotes::MissingTimestampError, target_stamp
56
55
  end
57
56
 
58
- tmpfile = run_with_tmpfile(editor, target_stamp.to_s, text)
57
+ tmpfile = Rbnotes.utils.run_with_tmpfile(editor, target_stamp.to_s, text)
59
58
  text = File.readlines(tmpfile, :chomp => true)
60
59
 
61
60
  unless text.empty?
@@ -83,7 +82,7 @@ module Rbnotes::Commands
83
82
  def help # :nodoc:
84
83
  puts <<HELP
85
84
  usage:
86
- #{Rbnotes::NAME} update [TIMESTAMP]
85
+ #{Rbnotes::NAME} update [-k|--keep] [TIMESTAMP]
87
86
 
88
87
  Updates the content of the note associated with given timestamp.
89
88
 
@@ -7,10 +7,12 @@ module Rbnotes
7
7
  # :stopdoc:
8
8
 
9
9
  module ErrMsg
10
- MISSING_ARGUMENT = "missing argument: %s"
11
- MISSING_TIMESTAMP = "missing timestamp: %s"
10
+ MISSING_ARGUMENT = "Missing argument: %s"
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"
15
+ INVALID_TIMESTAMP_PATTERN = "Invalid timestamp pattern: %s"
14
16
  end
15
17
 
16
18
  # :startdoc:
@@ -53,4 +55,24 @@ module Rbnotes
53
55
  super(ErrMsg::PROGRAM_ABORT % cmdline.join(" "))
54
56
  end
55
57
  end
58
+
59
+ ##
60
+ # An eeror raised when an unknown keyword was specified as a
61
+ # timestamp string pattern.
62
+
63
+ class UnknownKeywordError < Error
64
+ def initialize(keyword)
65
+ super(ErrMsg::UNKNOWN_KEYWORD % keyword)
66
+ end
67
+ end
68
+
69
+ ##
70
+ # An error raised when an invalid timestamp pattern was specified.
71
+
72
+ class InvalidTimestampPatternError < Error
73
+ def initialize(pattern)
74
+ super(ErrMsg::INVALID_TIMESTAMP_PATTERN % pattern)
75
+ end
76
+ end
77
+
56
78
  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,122 @@ 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
229
+
230
+ ##
231
+ # Enumerates all timestamp patterns in a week which contains a
232
+ # given timestamp as a day of the week.
233
+ #
234
+ # :call-seq:
235
+ # timestamp_patterns_in_week(timestamp) -> [Array of Strings]
236
+
237
+ def timestamp_patterns_in_week(timestamp)
238
+ dates_in_week(start_date_in_the_week(timestamp.time)).map { |date| timestamp_pattern(date) }
239
+ end
123
240
 
124
241
  # :stopdoc:
125
242
 
@@ -132,11 +249,74 @@ module Rbnotes
132
249
  }
133
250
  found.compact[0]
134
251
  end
135
- module_function :search_in_path
136
252
 
137
253
  def add_extension(basename)
138
254
  "#{basename}.md"
139
255
  end
140
- module_function :add_extension
256
+
257
+ def timestamp_pattern(date)
258
+ date.strftime("%Y%m%d")
259
+ end
260
+
261
+ def date_of_today
262
+ date(Time.now)
263
+ end
264
+
265
+ def date_of_yesterday
266
+ date(Time.now).prev_day
267
+ end
268
+
269
+ def date(time)
270
+ Date.new(time.year, time.mon, time.day)
271
+ end
272
+
273
+ def dates_in_this_week
274
+ dates_in_week(start_date_in_this_week)
275
+ end
276
+
277
+ def dates_in_last_week
278
+ dates_in_week(start_date_in_last_week)
279
+ end
280
+
281
+ def start_date_in_this_week
282
+ start_date_in_the_week(Time.now)
283
+ end
284
+
285
+ def start_date_in_last_week
286
+ start_date_in_this_week.prev_day(7)
287
+ end
288
+
289
+ def start_date_in_the_week(time)
290
+ parts = [:year, :mon, :day].map { |sym| time.send(sym) }
291
+ Date.new(*parts).prev_day(wday(time))
292
+ end
293
+
294
+ def wday(time)
295
+ (time.wday - 1) % 7
296
+ end
297
+
298
+ def dates_in_week(start_date)
299
+ dates = [start_date]
300
+ 1.upto(6) { |i| dates << start_date.next_day(i) }
301
+ dates
302
+ end
303
+
304
+ def truncate_str(str, size)
305
+ count = 0
306
+ result = ""
307
+ str.each_char { |c|
308
+ count += Unicode::DisplayWidth.of(c)
309
+ break if count > size
310
+ result << c
311
+ }
312
+ result
313
+ end
314
+
315
+ def remove_heading_markup(str)
316
+ str.sub(/^#+ +/, '')
317
+ end
318
+
319
+ # :startdoc:
320
+
141
321
  end
142
322
  end
@@ -1,4 +1,4 @@
1
1
  module Rbnotes
2
- VERSION = "0.4.5"
3
- RELEASE = "2020-11-12"
2
+ VERSION = "0.4.10"
3
+ RELEASE = "2020-11-20"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbnotes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - mnbi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-12 00:00:00.000000000 Z
11
+ date: 2020-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: textrepo
@@ -63,11 +63,13 @@ files:
63
63
  - lib/rbnotes.rb
64
64
  - lib/rbnotes/commands.rb
65
65
  - lib/rbnotes/commands/add.rb
66
+ - lib/rbnotes/commands/commands.rb
66
67
  - lib/rbnotes/commands/delete.rb
67
68
  - lib/rbnotes/commands/export.rb
68
69
  - lib/rbnotes/commands/help.rb
69
70
  - lib/rbnotes/commands/import.rb
70
71
  - lib/rbnotes/commands/list.rb
72
+ - lib/rbnotes/commands/pick.rb
71
73
  - lib/rbnotes/commands/search.rb
72
74
  - lib/rbnotes/commands/show.rb
73
75
  - lib/rbnotes/commands/update.rb