rbnotes 0.4.10 → 0.4.11

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: 7e6de4675ffb2b409f72f240e4509a6d254793d977a8dd68cc54cf6d0c839e3d
4
- data.tar.gz: 364d4deb47d049af7145024cfb3151639ea9cf0504963c1b72e5a15e465a84a6
3
+ metadata.gz: 4170ccc84305971d10f3726351cdc4138b5fabaa7293bc10a827ed0458f28b8c
4
+ data.tar.gz: 291efc32e2e65462e8c6996ecbbc7ffbb4e6887a5d0065bbb843d01dfbb8c78c
5
5
  SHA512:
6
- metadata.gz: bf90b255e23257a921f7a9254d4df5233f97394e73368f27f6a4a8e96989fa3aa76ab55fca69b201ecd9a15264d5d07cf974ee9b745854ca80d3c962accbe37a
7
- data.tar.gz: c9fca12a0a6a71562a968775b09e3ebf7bda202ce7251666ab7b727236c47f8b5981d7ee703b94bfc22fae038cc161f76b10ed413ca28bcac3a9d9e0417f7f83
6
+ metadata.gz: 1f6d2659c1a50f462867a53bff3f7b45e8b38035c3af44407b70d359c6af9409481f1033059384bd8d6b42d03981ce4f955ac87ee5fe7e99f5b50465d47f4e1b
7
+ data.tar.gz: f5949cfe44eef0951a80ec8949b6bb8a3440d5b483ddacdaf652d8b4dd258a7eb53db42c40290e9a2ebd6e3a1bf6e204f2382b41dd40b23ac8b271dd05547672
@@ -5,7 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
- Nothing to record here.
8
+ ## [0.4.11] - 2020-12-07
9
+ ### Added
10
+ - Add a new command `statistics`. (#73)
11
+ - limited features
12
+ - Add a completion file for `zsh`.
13
+ - a new file `etc/zsh/_rbnotes`
14
+
15
+ ### Changed
16
+ - Add a new option for `import` to use `mtime`. (#82)
17
+ - Add a feature to show multiple notes at once. (#79)
18
+
19
+ ### Fixed
20
+ - Fix issue #77: no error with a non-existing config file.
9
21
 
10
22
  ## [0.4.10] - 2020-11-20
11
23
  ### Added
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rbnotes (0.4.10)
4
+ rbnotes (0.4.11)
5
5
  textrepo (~> 0.5.4)
6
6
  unicode-display_width (~> 1.7)
7
7
 
@@ -0,0 +1,93 @@
1
+ #compdef rbnotes
2
+
3
+ local __rbnotes_cmd __rbnotes_debug
4
+
5
+ __rbnotes_process() {
6
+ }
7
+
8
+ function _rbnotes() {
9
+ local context curcontext=$curcontext stat line
10
+ typeset -A opt_args
11
+ local ret=1
12
+
13
+ _arguments \
14
+ -C \
15
+ '(- *)'{-v,--version}'[print version]' \
16
+ '(- *)'{-h,--help}'[show help]' \
17
+ '(- *)'{-c,--conf}'[config file]: :->conffile' \
18
+ '1: :__rbnotes_commands' \
19
+ '*:: :->args'
20
+
21
+ case $state in
22
+ (conffile)
23
+ _files -g "*.yml" && ret=0
24
+ ;;
25
+ (args)
26
+ case $words[1] in
27
+ (add)
28
+ _arguments \
29
+ -C \
30
+ '(-t --timestamp)'{-t,--timestamp}'[set timestamp]' \
31
+ '(-)*:: :->null_state' \
32
+ && ret=0
33
+ ;;
34
+ (export)
35
+ _directories && ret=0
36
+ ;;
37
+ (help)
38
+ _arguments \
39
+ -C \
40
+ '1: :__rbnotes_commands' \
41
+ && ret=0
42
+ ;;
43
+ (import)
44
+ _files -g '*.md' && ret=0
45
+ ;;
46
+ (list|pick)
47
+ _arguments \
48
+ -C \
49
+ '1: :__rbnotes_list_keywords' \
50
+ && ret=0
51
+ ;;
52
+ (update)
53
+ _arguments \
54
+ -C \
55
+ '(-k --keep)'{-k,--keep}'[keep timestamp]' \
56
+ '(-)*:: :->nul_state' \
57
+ && ret=0
58
+ ;;
59
+ esac
60
+ ;;
61
+ esac
62
+
63
+ return ret
64
+ }
65
+
66
+ __rbnotes_commands() {
67
+ local -a _cmds
68
+ _cmds=( $(rbnotes commands -d) )
69
+ _describe -t commands Commands _cmds
70
+ }
71
+
72
+ __rbnotes_list_keywords() {
73
+ local -a _kw _this_month _this_year
74
+ _this_month=$(date "+%Y%m")
75
+ _last_month=$(date -v-1m "+%Y%m")
76
+ _this_year=$(date "+%Y")
77
+ _kw=(
78
+ {to,today}':Today'
79
+ {ye,yesterday}':Yesterday'
80
+ {tw,this_week}':This week'
81
+ {lw,last_week}':Last week'
82
+ "${_this_month}:This month"
83
+ "${_last_month}:Last month"
84
+ "${_this_year}:This year"
85
+ )
86
+ _describe -t keywords Keywords _kw
87
+ }
88
+
89
+ _rbnotes "$@"
90
+
91
+ # Local Variables:
92
+ # mode: shell-script
93
+ # End:
@@ -57,6 +57,7 @@ rescue MissingArgumentError, MissingTimestampError,
57
57
  NoEditorError, ProgramAbortError,
58
58
  Textrepo::InvalidTimestampStringError,
59
59
  InvalidTimestampPatternError,
60
+ NoConfFileError,
60
61
  ArgumentError,
61
62
  Errno::EACCES => e
62
63
  puts e.message
@@ -8,6 +8,7 @@ module Rbnotes
8
8
  require_relative "rbnotes/conf"
9
9
  require_relative "rbnotes/utils"
10
10
  require_relative "rbnotes/commands"
11
+ require_relative "rbnotes/statistics"
11
12
 
12
13
  class << self
13
14
  def utils
@@ -61,10 +61,11 @@ Example usage:
61
61
  #{Rbnotes::NAME} commands [-d]
62
62
  #{Rbnotes::NAME} delete [TIMESTAMP]
63
63
  #{Rbnotes::NAME} export [TIMESTAMP [FILENAME]]
64
- #{Rbnotes::NAME} import FILE
64
+ #{Rbnotes::NAME} import [-m|--use-mtime] FILE
65
65
  #{Rbnotes::NAME} list [STAMP_PATTERN|KEYWORD]
66
66
  #{Rbnotes::NAME} search PATTERN [STAMP_PATTERN]
67
- #{Rbnotes::NAME} show [TIMESTAMP]
67
+ #{Rbnotes::NAME} show [TIMESTAMP...]
68
+ #{Rbnotes::NAME} statistics ([-y]|[-m])
68
69
  #{Rbnotes::NAME} update [-k] [TIMESTAMP]
69
70
 
70
71
  Further help for each command:
@@ -4,9 +4,12 @@ module Rbnotes::Commands
4
4
  # Imports a existing file which specified by the argument as a note.
5
5
  #
6
6
  # A timestamp is generated referring to the birthtime of the given
7
- # file. If birthtime is not available on the system, use mtime
7
+ # file. If birthtime is not available on the system, uses mtime
8
8
  # (modification time).
9
9
  #
10
+ # When the option, "-m" (or "--use-mtime") is specified, uses mtime
11
+ # instead of birthtime.
12
+ #
10
13
  # Occasionally, there is another note which has the same timestmap
11
14
  # in the repository. Then, tries to create a new timestamp with a
12
15
  # suffix. Unluckily, when such timestamp with a suffix already
@@ -25,11 +28,29 @@ module Rbnotes::Commands
25
28
  # execute([PATHNAME], Rbnotes::Conf or Hash) -> nil
26
29
 
27
30
  def execute(args, conf)
31
+ @opts = {}
32
+ while args.size > 0
33
+ arg = args.shift
34
+ case arg
35
+ when "-m", "--use-mtime"
36
+ @opts[:use_mtime] = true
37
+ else
38
+ args.unshift(arg)
39
+ break
40
+ end
41
+ end
42
+
28
43
  file = args.shift
29
44
  unless file.nil?
30
45
  st = File::Stat.new(file)
31
- btime = st.respond_to?(:birthtime) ? st.birthtime : st.mtime
32
- stamp = Textrepo::Timestamp.new(btime)
46
+ time = nil
47
+ if @opts[:use_mtime]
48
+ time = st.mtime
49
+ else
50
+ time = st.respond_to?(:birthtime) ? st.birthtime : st.mtime
51
+ end
52
+
53
+ stamp = Textrepo::Timestamp.new(time)
33
54
  puts "Import [%s] (timestamp [%s]) ..." % [file, stamp]
34
55
 
35
56
  repo = Textrepo.init(conf)
@@ -72,7 +93,7 @@ module Rbnotes::Commands
72
93
  puts "Cannot create a text into the repository with the" \
73
94
  " specified file [%s]." % file
74
95
  puts "For, the birthtime [%s] is identical to some notes" \
75
- " already exists in the reopsitory." % btime
96
+ " already exists in the reopsitory." % time
76
97
  puts "Change the birthtime of the target file, then retry."
77
98
  else
78
99
  puts "... Done."
@@ -86,7 +107,7 @@ module Rbnotes::Commands
86
107
  def help # :nodoc:
87
108
  puts <<HELP
88
109
  usage:
89
- #{Rbnotes::NAME} import FILE
110
+ #{Rbnotes::NAME} import [-m|--use-mtime] FILE
90
111
 
91
112
  Imports a existing file which specified by the argument as a note.
92
113
 
@@ -1,52 +1,97 @@
1
1
  module Rbnotes::Commands
2
2
 
3
3
  ##
4
- # Shows the content of the note specified by the argument. The
4
+ # Shows the content of the notes specified by arguments. Each
5
5
  # argument must be a string which can be converted into
6
6
  # Textrepo::Timestamp object.
7
7
  #
8
- # A string for Timestamp must be:
8
+ # A string for Textrepo::Timestamp must be:
9
9
  #
10
10
  # "20201106112600" : year, date, time and sec
11
11
  # "20201106112600_012" : with suffix
12
12
  #
13
- # If no argument is passed, reads the standard input for an argument.
13
+ # If no argument is passed, reads the standard input for arguments.
14
14
 
15
15
  class Show < Command
16
16
 
17
17
  def description # :nodoc:
18
- "Show the content of a note"
18
+ "Show the content of notes"
19
19
  end
20
20
 
21
21
  def execute(args, conf)
22
- stamp = Rbnotes.utils.read_timestamp(args)
23
-
22
+ stamps = Rbnotes.utils.read_multiple_timestamps(args)
24
23
  repo = Textrepo.init(conf)
25
- content = repo.read(stamp)
24
+
25
+ content = stamps.map { |stamp| [stamp, repo.read(stamp)] }.to_h
26
26
 
27
27
  pager = conf[:pager]
28
28
  unless pager.nil?
29
- require 'open3'
30
- Open3.pipeline_w(pager) { |stdin|
31
- stdin.puts content
32
- stdin.close
33
- }
29
+ puts_with_pager(pager, make_output(content))
34
30
  else
35
- puts content
31
+ puts make_output(content)
36
32
  end
37
33
  end
38
34
 
39
35
  def help # :nodoc:
40
36
  puts <<HELP
41
37
  usage:
42
- #{Rbnotes::NAME} show [TIMESTAMP]
38
+ #{Rbnotes::NAME} show [TIMESTAMP...]
43
39
 
44
- Show the content of given note. TIMESTAMP must be a fully qualified
40
+ Show the content of given notes. TIMESTAMP must be a fully qualified
45
41
  one, such "20201016165130" or "20201016165130_012" if it has a suffix.
46
42
 
47
43
  The command try to read its argument from the standard input when no
48
44
  argument was passed in the command line.
49
45
  HELP
50
46
  end
47
+
48
+ # :stopdoc:
49
+
50
+ private
51
+
52
+ def puts_with_pager(pager, output)
53
+ require "open3"
54
+ Open3.pipeline_w(pager) { |stdin|
55
+ stdin.puts output
56
+ stdin.close
57
+ }
58
+ end
59
+
60
+ require "io/console/size"
61
+
62
+ def make_output(content)
63
+ if content.size <= 1
64
+ return content.values[0]
65
+ end
66
+
67
+ _, column = IO.console_size
68
+ output = content.map { |timestamp, text|
69
+ ary = [make_heading(timestamp, [column, 72].min)]
70
+ ary.concat(text)
71
+ ary
72
+ }
73
+
74
+ output = insert_delimiter(output, "")
75
+ output.flatten
76
+ end
77
+
78
+ def make_heading(timestamp, column)
79
+ stamp_str = timestamp.to_s
80
+ length = column - (stamp_str.size + 2)
81
+ "#{stamp_str} #{Array.new(length, '-').join}"
82
+ end
83
+
84
+ def insert_delimiter(ary, delimiter = "")
85
+ result = []
86
+ ary.each { |e|
87
+ result << e
88
+ result << delimiter
89
+ }
90
+ result.delete_at(-1)
91
+ result
92
+ end
93
+
94
+ # :startdoc:
95
+
51
96
  end
52
97
  end
@@ -0,0 +1,55 @@
1
+ module Rbnotes::Commands
2
+ ##
3
+ # Shows statistics.
4
+
5
+ class Statistics < Command
6
+
7
+ def description # :nodoc:
8
+ "Show statistics values"
9
+ end
10
+
11
+ def execute(args, conf)
12
+ report = :total
13
+ while args.size > 0
14
+ arg = args.shift
15
+ case arg
16
+ when "-y", "--yearly"
17
+ report = :yearly
18
+ break
19
+ when "-m", "--monthly"
20
+ report = :monthly
21
+ break
22
+ else
23
+ args.unshift(arg)
24
+ raise ArgumentError, "invalid option or argument: %s" % args.join(" ")
25
+ end
26
+ end
27
+
28
+ stats = Rbnotes::Statistics.new(conf)
29
+ case report
30
+ when :yearly
31
+ stats.yearly_report
32
+ when :monthly
33
+ stats.monthly_report
34
+ else
35
+ stats.total_report
36
+ end
37
+ end
38
+
39
+ def help
40
+ puts <<HELP
41
+ usage:
42
+ #{Rbnotes::NAME} statistics ([-y|--yearly]|[-m|--monthly])
43
+
44
+ option:
45
+ -y, --yearly : print yearly report
46
+ -m, --monthly : print monthly report
47
+
48
+ Show statistics.
49
+
50
+ In the version #{Rbnotes::VERSION}, only number of notes is supported.
51
+ HELP
52
+ end
53
+
54
+ end
55
+ end
@@ -32,16 +32,18 @@ module Rbnotes
32
32
  DIRNAME_COMMON_CONF = ".config"
33
33
 
34
34
  def initialize(conf_path = nil) # :nodoc:
35
- @conf_path = conf_path || File.join(base_path, FILENAME_CONF)
36
-
35
+ @conf_path = conf_path
37
36
  @conf = {}
38
- if FileTest.exist?(@conf_path)
37
+
38
+ if use_default_values?
39
+ @conf.merge!(DEFAULT_VALUES)
40
+ else
41
+ @conf_path ||= default_conf_file
42
+ raise NoConfFileError, @conf_path unless File.exist?(@conf_path)
43
+
39
44
  yaml_str = File.open(@conf_path, "r") { |f| f.read }
40
45
  @conf = YAML.load(yaml_str)
41
- else
42
- @conf.merge(DEFAULT_VALUES)
43
46
  end
44
- self
45
47
  end
46
48
 
47
49
  def_delegators(:@conf,
@@ -99,10 +101,19 @@ module Rbnotes
99
101
  end
100
102
  return path
101
103
  end
102
- end
104
+
105
+ def default_conf_file
106
+ File.join(base_path, FILENAME_CONF)
107
+ end
108
+
109
+ def use_default_values?
110
+ @conf_path.nil? && !File.exist?(default_conf_file)
111
+ end
103
112
 
104
113
  # :startdoc:
105
114
 
115
+ end
116
+
106
117
  class << self
107
118
  ##
108
119
  # Gets the instance of Rbnotes::Conf. An optional argument is to
@@ -13,6 +13,7 @@ module Rbnotes
13
13
  PROGRAM_ABORT = "External program was aborted: %s"
14
14
  UNKNOWN_KEYWORD = "Unknown keyword: %s"
15
15
  INVALID_TIMESTAMP_PATTERN = "Invalid timestamp pattern: %s"
16
+ NO_CONF_FILE = "No configuration file: %s"
16
17
  end
17
18
 
18
19
  # :startdoc:
@@ -75,4 +76,14 @@ module Rbnotes
75
76
  end
76
77
  end
77
78
 
79
+ ##
80
+ # An error raised when the specified configuration file does not
81
+ # exist.
82
+
83
+ class NoConfFileError < Error
84
+ def initialize(filename)
85
+ super(ErrMsg::NO_CONF_FILE % filename)
86
+ end
87
+ end
88
+
78
89
  end
@@ -0,0 +1,101 @@
1
+ module Rbnotes
2
+ ##
3
+ # Calculates statistics of the repository.
4
+ class Statistics
5
+ include Enumerable
6
+
7
+ def initialize(conf)
8
+ @repo = Textrepo.init(conf)
9
+ @values = construct_values(@repo)
10
+ end
11
+
12
+ def total_report
13
+ puts @repo.entries.size
14
+ end
15
+
16
+ def yearly_report
17
+ self.each_year { |year, monthly_values|
18
+ num_of_notes = monthly_values.map { |_mon, values| values.size }.sum
19
+ puts "#{year}: #{num_of_notes}"
20
+ }
21
+ end
22
+
23
+ def monthly_report
24
+ self.each { |year, mon, values|
25
+ num_of_notes = values.size
26
+ puts "#{year}/#{mon}: #{num_of_notes}"
27
+ }
28
+ end
29
+
30
+ def each(&block)
31
+ if block.nil?
32
+ @values.map { |year, monthly_values|
33
+ monthly_values.each { |mon, values|
34
+ [year, mon, values]
35
+ }
36
+ }.to_enum(:each)
37
+ else
38
+ @values.each { |year, monthly_values|
39
+ monthly_values.each { |mon, values|
40
+ yield [year, mon, values]
41
+ }
42
+ }
43
+ end
44
+ end
45
+
46
+ def years
47
+ @values.keys
48
+ end
49
+
50
+ def months(year)
51
+ @values[year] || []
52
+ end
53
+
54
+ def each_year(&block)
55
+ if block.nil?
56
+ @values.map { |year, monthly_values|
57
+ [year, monthly_values]
58
+ }.to_enum(:each)
59
+ else
60
+ @values.each { |year, monthly_values|
61
+ yield [year, monthly_values]
62
+ }
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def construct_values(repo)
69
+ values = {}
70
+ repo.each { |timestamp, text|
71
+ value = StatisticValue.new(timestamp, text)
72
+ y = value.year
73
+ m = value.mon
74
+ values[y] ||= {}
75
+ values[y][m] ||= []
76
+
77
+ values[y][m] << value
78
+ }
79
+ values
80
+ end
81
+
82
+ class StatisticValue
83
+
84
+ attr_reader :lines
85
+
86
+ def initialize(timestamp, text)
87
+ @timestamp = timestamp
88
+ @lines = text.size
89
+ end
90
+
91
+ def year
92
+ @timestamp[:year]
93
+ end
94
+
95
+ def mon
96
+ @timestamp[:mon]
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -103,23 +103,16 @@ module Rbnotes
103
103
  end
104
104
 
105
105
  ##
106
- # Reads an argument from the IO object. Typically, it is intended
107
- # to be used with STDIN.
106
+ # Generates multiple Textrepo::Timestamp objects from the command
107
+ # line arguments. When no argument is given, try to read from
108
+ # STDIN.
108
109
  #
109
110
  # :call-seq:
110
- # read_arg(IO) -> String
111
+ # read_multiple_timestamps(args) -> [String]
111
112
 
112
- def read_arg(io)
113
- # assumes the reading line looks like:
114
- #
115
- # foo bar baz ...
116
- #
117
- # then, only the first string is interested
118
- begin
119
- io.gets.split(":")[0].rstrip
120
- rescue NoMethodError => _
121
- nil
122
- end
113
+ def read_multiple_timestamps(args)
114
+ strings = args.size < 1 ? read_multiple_args($stdin) : args
115
+ strings.map { |str| Textrepo::Timestamp.parse_s(str) }
123
116
  end
124
117
 
125
118
  ##
@@ -241,6 +234,41 @@ module Rbnotes
241
234
  # :stopdoc:
242
235
 
243
236
  private
237
+
238
+ ##
239
+ # Reads an argument from the IO object. Typically, it is intended
240
+ # to be used with STDIN.
241
+ #
242
+ # :call-seq:
243
+ # read_arg(IO) -> String
244
+
245
+ def read_arg(io)
246
+ read_multiple_args(io)[0]
247
+ end
248
+
249
+ ##
250
+ # Reads arguments from the IO object. Typically, it is intended
251
+ # to be used with STDIN.
252
+ #
253
+ # :call-seq:
254
+ # read_multiple_arg(IO) -> [String]
255
+
256
+ def read_multiple_args(io)
257
+ strings = io.readlines
258
+ strings.map { |str|
259
+ # assumes the reading line looks like:
260
+ #
261
+ # foo bar baz ...
262
+ #
263
+ # then, only the first string is interested
264
+ begin
265
+ str.split(":")[0].rstrip
266
+ rescue NoMethodError => _
267
+ nil
268
+ end
269
+ }.compact
270
+ end
271
+
244
272
  def search_in_path(name)
245
273
  search_paths = ENV["PATH"].split(":")
246
274
  found = search_paths.map { |path|
@@ -1,4 +1,4 @@
1
1
  module Rbnotes
2
- VERSION = "0.4.10"
3
- RELEASE = "2020-11-20"
2
+ VERSION = "0.4.11"
3
+ RELEASE = "2020-12-07"
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.10
4
+ version: 0.4.11
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-20 00:00:00.000000000 Z
11
+ date: 2020-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: textrepo
@@ -59,6 +59,7 @@ files:
59
59
  - conf/config.yml
60
60
  - conf/config_deve.yml
61
61
  - conf/config_test.yml
62
+ - etc/zsh/_rbnotes
62
63
  - exe/rbnotes
63
64
  - lib/rbnotes.rb
64
65
  - lib/rbnotes/commands.rb
@@ -72,9 +73,11 @@ files:
72
73
  - lib/rbnotes/commands/pick.rb
73
74
  - lib/rbnotes/commands/search.rb
74
75
  - lib/rbnotes/commands/show.rb
76
+ - lib/rbnotes/commands/statistics.rb
75
77
  - lib/rbnotes/commands/update.rb
76
78
  - lib/rbnotes/conf.rb
77
79
  - lib/rbnotes/error.rb
80
+ - lib/rbnotes/statistics.rb
78
81
  - lib/rbnotes/utils.rb
79
82
  - lib/rbnotes/version.rb
80
83
  - rbnotes.gemspec