rbnotes 0.4.5 → 0.4.10

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