rbnotes 0.2.1

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e3b44e20631a0216391b389aee7faca4ecebe31502bbff92606c938023a98240
4
+ data.tar.gz: 4a39041ad77631da13be01ebde0745edd38f22f52b5c9ad2ae1a002053739c1b
5
+ SHA512:
6
+ metadata.gz: e98fabf28f56131caf2ec40c1111aa74a0819d923937cd033eeed6921c5ce43102657e8c3d787a8b9718b3057e7505921d2f7d316a327a041e80dcc2bce4b2ae
7
+ data.tar.gz: 672cd941dd68c545056c752a714bc29df9a9de5b14ce00b8c6568562df619e796c7cea77b376380bb3c32c06d680f834552c3c6367033bf2ab02b7460c4e1b0f
@@ -0,0 +1,56 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.2
6
+ before_install: gem install bundler -v 2.1.4
@@ -0,0 +1,40 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/)
5
+ and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+ Nothing to record here.
9
+
10
+ ## [0.2.1] - 2020-10-25
11
+ ### Added
12
+ - Add feature to load the configuration from an external file.
13
+ - Add description about the configuration file in README.md
14
+
15
+ ## [0.2.0] - 2020-10-23
16
+ ### Added
17
+ - Add more commands (add/update/delete).
18
+ - Add and update commands use a external editor to edit a note.
19
+ - Delete command remove the specified note from the repository.
20
+ - Add a new task into `Rakefile` to generate RI docs.
21
+ - The intention of the task is to verify RI docs.
22
+
23
+ ### Fixed
24
+ - Refactor some tests.
25
+
26
+ ## [0.1.3] - 2020-10-15
27
+ ### Fixed
28
+ - Add help text for the `conf` command.
29
+
30
+ ## [0.1.2] - 2020-10-15
31
+ ### Fixed
32
+ - Adapt the API change in `textrepo` (0.4.0).
33
+
34
+ ## [0.1.0] - 2020-10-12
35
+ ### Added
36
+ - Import files those are part of `examples` in the `textrepo` gem.
37
+ - All commands in the example version works fine.
38
+ - Wrote tests for all commands.
39
+ - All files those are generated by `bundler`.
40
+ - Add CHANGELOG.md to start recording changes.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rbnotes.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 13.0"
7
+ gem "minitest", "~> 5.0"
8
+
9
+ gem 'textrepo', "~> 0.4"
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rbnotes (0.2.1)
5
+ textrepo (~> 0.4)
6
+ unicode-display_width (~> 1.7)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ minitest (5.14.2)
12
+ rake (13.0.1)
13
+ textrepo (0.4.2)
14
+ unicode-display_width (1.7.0)
15
+
16
+ PLATFORMS
17
+ ruby
18
+
19
+ DEPENDENCIES
20
+ minitest (~> 5.0)
21
+ rake (~> 13.0)
22
+ rbnotes!
23
+ textrepo (~> 0.4)
24
+
25
+ BUNDLED WITH
26
+ 2.1.4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 mnbi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,156 @@
1
+ # Rbnotes
2
+
3
+ [![Build Status](https://travis-ci.org/mnbi/rbnotes.svg?branch=main)](https://travis-ci.org/mnbi/rbnotes)
4
+
5
+ Rbnotes is a simple utility to write a note in the single repository.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'rbnotes, github: 'mnbi/rbnotes, branch: 'main'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ ## Usage
20
+
21
+ General syntax is:
22
+
23
+ ``` shell
24
+ rbnotes [global_opts] [command] [command_opts] [args]
25
+ ```
26
+
27
+ ### Commands
28
+
29
+ - import
30
+ - imports existing files
31
+ - list
32
+ - lists notes in the repository with their timestamps and subject
33
+ - show
34
+ - shows the content of a note
35
+ - add
36
+ - adds a new note to the repository using an external editor
37
+ - update
38
+ - update the content of the specified note using an external editor
39
+ - delete
40
+ - deletes the specified note form the repository
41
+
42
+ ### Configuration file
43
+
44
+ The `rbnotes` command reads the configuration file during its startup.
45
+ This section describes the specification of the configuration file.
46
+
47
+ #### Location
48
+
49
+ Searches the configuration file in the following order:
50
+
51
+ 1. `$XDG_CONFIG_HOME/rbnotes/config.yml`
52
+ 2. `$HOME/.config/rbnotes/config.yml`
53
+
54
+ None of them is found, then the default configuration is used.
55
+
56
+ #### Content
57
+
58
+ The format of the configuration file must be written in YAML.
59
+
60
+ The configuration of `rbnotes` is represented as a Hash object in the
61
+ program code. So, each line of the configuration YAML looks like:
62
+
63
+ > name: value
64
+
65
+ Name must be written as Ruby's symbol, such `:repository_type`. On
66
+ the other hand, value will be written in 2 types. Some are Ruby's
67
+ symbols, the others are strings surrounded with double/single
68
+ quotations.
69
+
70
+ A real example:
71
+
72
+ ``` yaml
73
+ ---
74
+ :run_mode: :development
75
+ :repository_type: :file_system
76
+ :repository_name: "notes"
77
+ :repository_base: "~"
78
+ :pager: "bat"
79
+ :editor: "/usr/local/bin/emacsclient"
80
+ ```
81
+
82
+ #### Variables
83
+
84
+ ##### :run-mode (mandatory)
85
+
86
+ - :production (default)
87
+ - :development
88
+ - :test
89
+
90
+ The run-mode affects to behavior of `rbnotes`, such logging
91
+ information, the location of the repository, ..., etc.
92
+
93
+ ##### :repository_type (mandatory)
94
+
95
+ - :file_system (default)
96
+
97
+ This value depends on classes those derived from
98
+ `Textrepo::Repository` of `textrepo`. Currently (`textrepo` 0.4.x),
99
+ Textrepo::FileSystemRepository is the only one class usable for
100
+ `rbnotes`.
101
+
102
+ ##### :repository_name (mandatory)
103
+
104
+ User can set an arbitrary string as this value. However, the value
105
+ will be used as a part of the repository location in the file system.
106
+ Some characters in the string may cause a problem.
107
+
108
+ In addition to this, the run-mode affects the actual value when it is
109
+ used in the program.
110
+
111
+ | original | :production | :development | :test |
112
+ |:------ |:------------|:-------------|:-----------|
113
+ | notes | notes | notes_dev | notes_test |
114
+
115
+ ##### :repository_base (mandatory)
116
+
117
+ This value is used as a base directory of the repository. That is,
118
+ the values constructs the root path of the repository with
119
+ the `:repository_name` value. It would be:
120
+
121
+ > :repository_base/:repository_name
122
+
123
+ The value must be an absolute path. The short-hand notation of the
124
+ home directory ("~") is usable.
125
+
126
+ ##### Miscellaneous variables (optional)
127
+
128
+ - :pager : specify a pager program
129
+ - :editor : specify a editor program
130
+
131
+ ##### Default values for mandatory variables
132
+
133
+ All mandatory variables have their default values. Here is the list
134
+ of them:
135
+
136
+ | variable | default value |
137
+ |:-----------------|:--------------|
138
+ | :run_mode | :production |
139
+ | :repository_type | :file_system |
140
+ | :repository_name | "notes" |
141
+ | :repository_base | "~" |
142
+
143
+ ## Development
144
+
145
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
146
+
147
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
148
+
149
+ ## Contributing
150
+
151
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mnbi/rbnotes.
152
+
153
+
154
+ ## License
155
+
156
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,30 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
11
+
12
+ task test: [:clean, :setup_test]
13
+
14
+ desc "Setup test data"
15
+ task :setup_test do
16
+ print "Set up to execute tests..."
17
+ load("test/fixtures/setup_test_repo.rb", true)
18
+ puts "done."
19
+ end
20
+
21
+ CLEAN << "test/sandbox"
22
+ CLOBBER << "test/fixtures/test_repo"
23
+
24
+ require "rdoc/task"
25
+
26
+ RDoc::Task.new do |rdoc|
27
+ rdoc.generator = "ri"
28
+ rdoc.rdoc_dir = "doc"
29
+ rdoc.rdoc_files.include("lib/**/*.rb")
30
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rbnotes"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rbnotes"
4
+
5
+ include Rbnotes
6
+
7
+ DEBUG = true
8
+
9
+ class App
10
+ def run(args)
11
+ cmd = args.shift
12
+ Commands.load(cmd).execute(args, Rbnotes.conf)
13
+ end
14
+ end
15
+
16
+ app = App.new
17
+ begin
18
+ app.run(ARGV)
19
+ rescue MissingArgumentError, MissingTimestampError,
20
+ NoEditorError, ProgramAbortError => e
21
+ puts e.message
22
+ exit 1
23
+ end
@@ -0,0 +1,9 @@
1
+ require "textrepo"
2
+
3
+ module Rbnotes
4
+ require_relative "rbnotes/version"
5
+ require_relative "rbnotes/error"
6
+ require_relative "rbnotes/conf"
7
+ require_relative "rbnotes/utils"
8
+ require_relative "rbnotes/commands"
9
+ end
@@ -0,0 +1,144 @@
1
+ module Rbnotes
2
+ ##
3
+ # This module defines all command classes of rbnotes. Each command
4
+ # class must be derived from Rbnotes::Commands::Command class.
5
+ module Commands
6
+ ##
7
+ # The base class for a command class.
8
+ class Command
9
+ ##
10
+ # :call-seq:
11
+ # Array, Hash -> nil
12
+ # - Array: arguments for each command
13
+ # - Hash : rbnotes configuration
14
+ #
15
+ def execute(args, conf)
16
+ Builtins::DEFAULT_CMD.new.execute(args, conf)
17
+ end
18
+ end
19
+
20
+ # :stopdoc:
21
+
22
+ # Built-in commands:
23
+ # - repo: prints the absolute path of the repository.
24
+ # - conf: prints all of the current configuration settings.
25
+ # - stamp: converts given TIME_STR into a timestamp.
26
+ # - time: converts given STAMP into a time string.
27
+ module Builtins
28
+ class Help < Command
29
+ def execute(_, _)
30
+ puts <<USAGE
31
+ usage: rbnotes [command] [args]
32
+
33
+ command:
34
+ import FILE : import a FILE into the repository
35
+ list NUM : list NUM notes
36
+ show STAMP : show the note specified with STAMP
37
+ delete STAMP : delete the note specified with STAMP
38
+
39
+ conf : print the current configuraitons
40
+ repo : print the repository path
41
+ stamp TIME_STR : convert TIME_STR into a timestamp
42
+ time STAMP : convert STAMP into a time string
43
+ version : print version
44
+ help : show help
45
+ USAGE
46
+ end
47
+ end
48
+
49
+ class Version < Command
50
+ def execute(_, _)
51
+ rbnotes_version = "rbnotes #{Rbnotes::VERSION} (#{Rbnotes::RELEASE})"
52
+ textrepo_version = "textrepo #{Textrepo::VERSION}"
53
+ puts "#{rbnotes_version} [#{textrepo_version}]"
54
+ end
55
+ end
56
+
57
+ class Repo < Command
58
+ def execute(_, conf)
59
+ name = conf[:repository_name]
60
+ base = conf[:repository_base]
61
+ type = conf[:repository_type]
62
+
63
+ puts case type
64
+ when :file_system
65
+ "#{base}/#{name}"
66
+ else
67
+ "#{base}/#{name}"
68
+ end
69
+ end
70
+ end
71
+
72
+ class Conf < Command
73
+ def execute(_, conf)
74
+ conf.keys.sort.each { |k|
75
+ puts "#{k}=#{conf[k]}"
76
+ }
77
+ end
78
+ end
79
+
80
+ require "time"
81
+
82
+ class Stamp < Command
83
+ def execute(args, _)
84
+ time_str = args.shift
85
+ unless time_str.nil?
86
+ puts Textrepo::Timestamp.new(::Time.parse(time_str)).to_s
87
+ else
88
+ puts "not specified TIME_STR"
89
+ super
90
+ end
91
+ end
92
+ end
93
+
94
+ class Time < Command
95
+ def execute(args, _)
96
+ stamp = args.shift
97
+ unless stamp.nil?
98
+ puts ::Time.new(*Textrepo::Timestamp.split_stamp(stamp).map(&:to_i)).to_s
99
+ else
100
+ puts "not specified STAMP"
101
+ super
102
+ end
103
+ end
104
+ end
105
+
106
+ DEFAULT_CMD = Help
107
+ end
108
+
109
+ DEFAULT_CMD_NAME = "help"
110
+
111
+ # :startdoc:
112
+
113
+ class << self
114
+ ##
115
+ # Loads a class to perfom the command, then returns an instance
116
+ # of the class.
117
+ #
118
+ # :call-seq:
119
+ # "import" -> an object of Rbnotes::Commnads::Import
120
+ # "list" -> an object of Rbnotes::Commands::List
121
+ # "show" -> an object of Rbnotes::Commands::Show
122
+ #
123
+ def load(cmd_name)
124
+ cmd_name ||= DEFAULT_CMD_NAME
125
+ klass_name = cmd_name.capitalize
126
+
127
+ klass = nil
128
+ if Builtins.const_defined?(klass_name, false)
129
+ klass = Builtins::const_get(klass_name, false)
130
+ else
131
+ begin
132
+ require_relative "commands/#{cmd_name}"
133
+ klass = const_get(klass_name, false)
134
+ rescue LoadError => _
135
+ STDERR.puts "unknown command: #{cmd_name}"
136
+ klass = Builtins::DEFAULT_CMD
137
+ end
138
+ end
139
+ klass.new
140
+ end
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,49 @@
1
+ module Rbnotes::Commands
2
+ ##
3
+ # Adds a new note to the repository. A new timestamp is generated
4
+ # at the execution time, then it is attached to the note. If the
5
+ # timestamp has already existed in the repository, the command
6
+ # fails.
7
+ #
8
+ # This command starts the external editor program to prepare text to
9
+ # store. The editor program will be searched in the following order:
10
+ #
11
+ # 1. conf[:editor] (conf is the 1st arg of execute method)
12
+ # 2. ENV["EDITOR"]
13
+ # 3. "nano"
14
+ # 4. "vi"
15
+ #
16
+ # If none of the above editor is available, the command fails.
17
+ #
18
+ class Add < Command
19
+ include ::Rbnotes::Utils
20
+
21
+ def execute(args, conf)
22
+ newstamp = Textrepo::Timestamp.new(Time.now)
23
+
24
+ candidates = [conf[:editor], ENV["EDITOR"], "nano", "vi"].compact
25
+ editor = find_program(candidates)
26
+ raise Rbnotes::NoEditorError, candidates if editor.nil?
27
+
28
+ tmpfile = run_with_tmpfile(editor, newstamp.to_s)
29
+ text = File.readlines(tmpfile)
30
+
31
+ repo = Textrepo.init(conf)
32
+ begin
33
+ repo.create(newstamp, text)
34
+ rescue Textrepo::DuplicateTimestampError => e
35
+ puts e.message
36
+ puts "Just wait a second, then retry."
37
+ rescue Textrepo::EmptyTextError => e
38
+ puts e.message
39
+ rescue StandardError => e
40
+ puts e.message
41
+ else
42
+ puts "Add a note [%s]" % newstamp.to_s
43
+ ensure
44
+ # Don't forget to remove the temporary file.
45
+ File.delete(tmpfile)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ # :markup: markdown
2
+
3
+ # Delete command deletes one note in the repository, which specified
4
+ # with a given timestamp string. The timestamp string must be a fully
5
+ # qualified one, like "20201016165130". The argument to specify a
6
+ # note is mandatory. If no argument was passed, it would print help
7
+ # message and exit.
8
+ #
9
+ # It does nothing when the specified note does not exist except to
10
+ # print error message.
11
+
12
+ # :stopdoc:
13
+
14
+ module Rbnotes
15
+ class Commands::Delete < Commands::Command
16
+ def execute(args, conf)
17
+ stamp_str = args.shift
18
+ unless stamp_str.nil?
19
+ repo = Textrepo.init(conf)
20
+ begin
21
+ stamp = Textrepo::Timestamp.parse_s(stamp_str)
22
+ repo.delete(stamp)
23
+ rescue Textrepo::MissingTimestampError => e
24
+ puts e.message
25
+ rescue StandardError => e
26
+ puts e.message
27
+ else
28
+ puts "Delete [%s]" % stamp.to_s
29
+ end
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ module Rbnotes
2
+ class Commands::Import < Commands::Command
3
+ def execute(args, conf)
4
+ file = args.shift
5
+ unless file.nil?
6
+ st = File::Stat.new(file)
7
+ btime = st.respond_to?(:birthtime) ? st.birthtime : st.mtime
8
+ stamp = Textrepo::Timestamp.new(btime)
9
+ puts "Import [%s] (timestamp [%s]) ..." % [file, stamp]
10
+
11
+ repo = Textrepo.init(conf)
12
+ content = nil
13
+ File.open(file, "r") {|f| content = f.readlines(chomp: true)}
14
+
15
+ count = 0
16
+ while count <= 999
17
+ begin
18
+ repo.create(stamp, content)
19
+ break # success to create a note
20
+ rescue Textrepo::DuplicateTimestampError => _
21
+ puts "A text with the timestamp [%s] has been already exists" \
22
+ " in the repository." % stamp
23
+
24
+ repo_text = repo.read(stamp)
25
+ if content == repo_text
26
+ # if the content is the same to the target file,
27
+ # the specified file has been already imported.
28
+ # Then, there is nothing to do. Just exit.
29
+ puts "The note [%s] in the repository exactly matches" \
30
+ " the specified file." % stamp
31
+ puts "It seems there is no need to import the file [%s]." % file
32
+ exit # normal end
33
+ else
34
+ puts "The text in the repository does not match the" \
35
+ " specified file."
36
+ count += 1
37
+ stamp = Textrepo::Timestamp.new(stamp.time, count)
38
+ puts "Try to create a note again with a new " \
39
+ "timestamp [%s]." % stamp
40
+ end
41
+ rescue Textrepo::EmptyTextError => _
42
+ puts "... aborted."
43
+ puts "The specified file is empyt."
44
+ exit 1 # error
45
+ end
46
+ end
47
+ if count > 999
48
+ puts "Cannot create a text into the repository with the" \
49
+ " specified file [%s]." % file
50
+ puts "For, the birthtime [%s] is identical to some notes" \
51
+ " already exists in the reopsitory." % btime
52
+ puts "Change the birthtime of the target file, then retry."
53
+ else
54
+ puts "... Done."
55
+ end
56
+ else
57
+ puts "not supecified FILE"
58
+ super
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,57 @@
1
+ require "unicode/display_width"
2
+ require "io/console/size"
3
+
4
+ module Rbnotes
5
+ class Commands::List < Commands::Command
6
+ def execute(args, conf)
7
+ @row, @column = IO.console_size
8
+ max = (args.shift || @row - 3).to_i
9
+
10
+ @repo = Textrepo.init(conf)
11
+ notes = @repo.entries.sort{|a, b| b <=> a}
12
+ notes[0, max].each { |timestamp_str|
13
+ puts make_headline(timestamp_str)
14
+ }
15
+ end
16
+
17
+ private
18
+ TIMESTAMP_STR_MAX_WIDTH = "yyyymoddhhmiss_sfx".size
19
+ # Makes a headline with the timestamp and subject of the notes, it
20
+ # looks like as follows:
21
+ #
22
+ # |<------------------ console column size --------------------->|
23
+ # +-- timestamp ---+ +--- subject (the 1st line of each note) --+
24
+ # | | | |
25
+ # 20101010001000_123: # I love Macintosh. [EOL]
26
+ # 20100909090909_999: # This is very very long long loooong subje[EOL]
27
+ # ++
28
+ # ^--- delimiter (2 characters)
29
+ #
30
+ # The subject part will truncate when it is long.
31
+ def make_headline(timestamp_str)
32
+
33
+ delimiter = ": "
34
+ subject_width = @column - TIMESTAMP_STR_MAX_WIDTH - delimiter.size - 1
35
+
36
+ subject = @repo.read(Textrepo::Timestamp.parse_s(timestamp_str))[0]
37
+ prefix = '# '
38
+ subject = prefix + subject.lstrip if subject[0, 2] != prefix
39
+
40
+ ts_part = "#{timestamp_str} "[0..(TIMESTAMP_STR_MAX_WIDTH - 1)]
41
+ sj_part = truncate_str(subject, subject_width)
42
+
43
+ ts_part + delimiter + sj_part
44
+ end
45
+
46
+ def truncate_str(str, size)
47
+ count = 0
48
+ result = ""
49
+ str.each_char { |c|
50
+ count += Unicode::DisplayWidth.of(c)
51
+ break if count > size
52
+ result << c
53
+ }
54
+ result
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ module Rbnotes
2
+ class Commands::Show < Commands::Command
3
+ def execute(args, conf)
4
+ stamp_str = args.shift
5
+ unless stamp_str.nil?
6
+ repo = Textrepo.init(conf)
7
+ stamp = Textrepo::Timestamp.parse_s(stamp_str)
8
+ content = repo.read(stamp)
9
+
10
+ pager = conf[:pager]
11
+ unless pager.nil?
12
+ require 'open3'
13
+ Open3.pipeline_w(pager) { |stdin|
14
+ stdin.puts content
15
+ stdin.close
16
+ }
17
+ else
18
+ puts content
19
+ end
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,75 @@
1
+ module Rbnotes::Commands
2
+ ##
3
+ # Updates the content of the note associated with given timestamp.
4
+ # Actual modification is done interactively by the external editor.
5
+ # The timestamp associated with the note will be updated to new one,
6
+ # which is generated while the command exection.
7
+ #
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.
16
+ #
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.
20
+ #
21
+ # If none of editors is available, the command fails.
22
+ #
23
+ class Update < Command
24
+ include ::Rbnotes::Utils
25
+
26
+ ##
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.
30
+ #
31
+ # :call-seq:
32
+ # "20201020112233" -> "20201021123400"
33
+ #
34
+ def execute(args, conf)
35
+ raise Rbnotes::MissingArgumentError, args if args.size < 1
36
+
37
+ target_stamp = nil
38
+ begin
39
+ target_stamp = Textrepo::Timestamp.parse_s(args.shift)
40
+ rescue ArgumentError => e
41
+ raise Rbnotes::MissingArgumentError, args
42
+ end
43
+
44
+ editor = find_editor(conf[:editor])
45
+
46
+ repo = Textrepo.init(conf)
47
+
48
+ text = nil
49
+ begin
50
+ text = repo.read(target_stamp)
51
+ rescue Textrepo::MissingTimestampError => _
52
+ raise Rbnotes::MissingTimestampError, target_stamp
53
+ end
54
+
55
+ tmpfile = run_with_tmpfile(editor, target_stamp.to_s, text)
56
+ text = File.readlines(tmpfile)
57
+
58
+ unless text.empty?
59
+ newstamp = nil
60
+ begin
61
+ newstamp = repo.update(target_stamp, text)
62
+ rescue StandardError => e
63
+ puts e.message
64
+ else
65
+ puts "Update the note [%s -> %s]" % [target_stamp, newstamp]
66
+ ensure
67
+ # Don't forget to remove the temporary file.
68
+ File.delete(tmpfile)
69
+ end
70
+ else
71
+ puts "Nothing is updated, since the specified content is empty."
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,118 @@
1
+ require "forwardable"
2
+ require "yaml"
3
+
4
+ module Rbnotes
5
+ ##
6
+ # Holds the configuration settings. Each value of setting can be
7
+ # retrieved like a Hash object. Here is some examples.
8
+ #
9
+ # conf = Rbnotes.conf
10
+ # type = conf[:repository_type]
11
+ # name = conf[:repository_name]
12
+ # base = conf[:repository_base]
13
+
14
+ class Conf
15
+ extend Forwardable
16
+ include Enumerable
17
+
18
+ ##
19
+ # Name of the file to store configuration settings.
20
+
21
+ FILENAME_CONF = "config.yml"
22
+
23
+ ##
24
+ # Name of the directory indicates which belongs to "rbnotes".
25
+
26
+ DIRNAME_RBNOTES = "rbnotes"
27
+
28
+ ##
29
+ # Name of the directory which is used to indicate to put
30
+ # configuration files. Many tools use this name as the role.
31
+
32
+ DIRNAME_COMMON_CONF = ".config"
33
+
34
+ def initialize(conf_path = nil) # :nodoc:
35
+ @conf_path = conf_path || File.join(base_path, FILENAME_CONF)
36
+
37
+ @conf = {}
38
+ if FileTest.exist?(@conf_path)
39
+ yaml_str = File.open(@conf_path, "r") { |f| f.read }
40
+ @conf = YAML.load(yaml_str)
41
+ else
42
+ @conf.merge(DEFAULT_VALUES)
43
+ end
44
+ self
45
+ end
46
+
47
+ def_delegators(:@conf,
48
+ :empyt?, :keys, :values,
49
+ :has_key?, :include?, :key?, :member?,
50
+ :length, :size,
51
+ :to_s,
52
+ :each, :each_pair)
53
+
54
+ ##
55
+ # Retrieves the value for the given key.
56
+ #
57
+ # :call-seq:
58
+ # self[sym] -> value
59
+
60
+ def [](sym)
61
+ mode = @conf[:run_mode] || :production
62
+ value = @conf[sym] || DEFAULT_VALUES[sym]
63
+ if [:repository_name].include?(sym)
64
+ value += MODE_POSTFIX[mode]
65
+ end
66
+ value
67
+ end
68
+
69
+ # :stopdoc:
70
+
71
+ def initialize_copy(_)
72
+ @conf = @conf.dup
73
+ end
74
+
75
+ def freeze; @conf.freeze; super; end
76
+ def taint; @conf.taint; super; end
77
+ def untaint; @conf.untaint; super; end
78
+
79
+ private
80
+ DEFAULT_VALUES = {
81
+ :repository_type => :file_system,
82
+ :repository_name => "notes",
83
+ :repository_base => "~",
84
+ }
85
+
86
+ MODE_POSTFIX = {
87
+ :production => "",
88
+ :development => "_deve",
89
+ :test => "_test",
90
+ }
91
+
92
+ def base_path
93
+ path = nil
94
+ xdg, user = ["XDG_CONFIG_HOME", "HOME"].map{|n| ENV[n]}
95
+ if xdg
96
+ path = File.join(File.expand_path(xdg), DIRNAME_RBNOTES)
97
+ else
98
+ path = File.join(user, DIRNAME_COMMON_CONF, DIRNAME_RBNOTES)
99
+ end
100
+ return path
101
+ end
102
+ end
103
+
104
+ # :startdoc:
105
+
106
+ class << self
107
+ ##
108
+ # Gets the instance of Rbnotes::Conf. An optional argument is to
109
+ # specify the absolute path for the configuration file.
110
+ #
111
+ # :call-seq:
112
+ # conf(String) => Rbnotes::Conf
113
+
114
+ def conf(conf_path = nil)
115
+ Conf.new(conf_path)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,54 @@
1
+ module Rbnotes
2
+ ##
3
+ # A base class for each error class of rbnotes.
4
+ #
5
+ class Error < StandardError; end
6
+
7
+ # :stopdoc:
8
+ module ErrMsg
9
+ MISSING_ARGUMENT = "missing argument: %s"
10
+ MISSING_TIMESTAMP = "missing timestamp: %s"
11
+ NO_EDITOR = "No editor is available: %s"
12
+ PROGRAM_ABORT = "External program was aborted: %s"
13
+ end
14
+
15
+ # :startdoc:
16
+
17
+ ##
18
+ # An error raised if an essential argument was missing.
19
+ #
20
+ class MissingArgumentError < Error
21
+ def initialize(args)
22
+ super(ErrMsg::MISSING_ARGUMENT % args.to_s)
23
+ end
24
+ end
25
+
26
+ ##
27
+ # An error raised if a given timestamp was not found in the
28
+ # repository.
29
+ #
30
+ class MissingTimestampError < Error
31
+ def initialize(timestamp)
32
+ super(ErrMsg::MISSING_TIMESTAMP % timestamp)
33
+ end
34
+ end
35
+
36
+ ##
37
+ # An error raised if no external editor is available to edit a note,
38
+ # even "nano" or "vi".
39
+ #
40
+ class NoEditorError < Error
41
+ def initialize(names)
42
+ super(ErrMsg::NO_EDITOR % names.to_s)
43
+ end
44
+ end
45
+
46
+ ##
47
+ # An error raised when a external program such an editor was aborted
48
+ # during its execution.
49
+ class ProgramAbortError < Error
50
+ def initialize(cmdline)
51
+ super(ErrMsg::PROGRAM_ABORT % cmdline.join(" "))
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,107 @@
1
+ require "pathname"
2
+ require "tmpdir"
3
+
4
+ module Rbnotes
5
+ ##
6
+ # Defines several utility methods those are intended to be used in
7
+ # Rbnotes classes.
8
+ #
9
+ module Utils
10
+
11
+ ##
12
+ # Finds a external editor program which is specified with the
13
+ # argument, then returns the absolute path of the editor. If the
14
+ # specified editor was not found, then search default editors in
15
+ # the command search paths (i.e. `ENV["PATH"]). See also the
16
+ # document for `find_program`.
17
+ #
18
+ # The default editors to search in the search paths are:
19
+ #
20
+ # 1. ENV["EDITOR"]
21
+ # 2. "nano"
22
+ # 3. "vi"
23
+ #
24
+ # When all the default editors were not found, returns `nil`.
25
+ #
26
+ def find_editor(preferred_editor)
27
+ find_program([preferred_editor, ENV["EDITOR"], "nano", "vi"].compact)
28
+ end
29
+ module_function :find_editor
30
+
31
+ ##
32
+ # Finds a executable program in given names. When the executable
33
+ # was found, it stops searching then returns an absolute path of
34
+ # the executable.
35
+ #
36
+ # The actual searching is done in 2 cases. That is, a given name is:
37
+ #
38
+ # 1. an absolute path:
39
+ # returns the path itself if it exists and is executable.
40
+ # 2. just a program name:
41
+ # searchs the name in the search paths (ENV["PATH"]);
42
+ # if it is found in a path, construct an absolute path from
43
+ # the name and the path, then returns the path.
44
+ #
45
+ # :call-seq:
46
+ # ["nano", "vi"] -> "/usr/bin/nano"
47
+ # ["vi", "/usr/local/bin/emacs"] -> "/usr/bin/vi"
48
+ # ["/usr/local/bin/emacs", "vi"] -> "/usr/bin/vi" (if emacs doesn't exist)
49
+ # ["/usr/local/bin/emacs", "vi"] -> "/usr/local/bin/emacs" (if exists)
50
+ #
51
+ def find_program(names)
52
+ names.each { |name|
53
+ pathname = Pathname.new(name)
54
+ if pathname.absolute?
55
+ return pathname.to_path if pathname.exist? && pathname.executable?
56
+ else
57
+ abs = search_in_path(name)
58
+ return abs unless abs.nil?
59
+ end
60
+ }
61
+ nil
62
+ end
63
+ module_function :find_program
64
+
65
+ ##
66
+ # Executes the program with passing the given filename as argument.
67
+ # The file will be created into `Dir.tmpdir`.
68
+ #
69
+ # If initial_content is not nil, it must be an array of strings
70
+ # then it provides the initial content of a temporary file.
71
+ #
72
+ # :call-seq:
73
+ # "/usr/bin/nano", "20201021131300.md", nil -> "/somewhere/tmpdir/20201021131300.md"
74
+ # "/usr/bin/vi", "20201021131301.md", ["apple\n", "orange\n"] -> "/somewhere/tmpdir/20201021131301.md"
75
+ #
76
+ def run_with_tmpfile(prog, filename, initial_content = nil)
77
+ tmpfile = File.expand_path(add_extension(filename), Dir.tmpdir)
78
+
79
+ unless initial_content.nil?
80
+ File.open(tmpfile, "w") {|f| f.print(initial_content.join("\n"))}
81
+ end
82
+
83
+ rc = system(prog, tmpfile)
84
+ raise ProgramAbortError, [prog, tmpfile] unless rc
85
+ tmpfile
86
+ end
87
+ module_function :run_with_tmpfile
88
+
89
+ # :stopdoc:
90
+
91
+ private
92
+ def search_in_path(name)
93
+ search_paths = ENV["PATH"].split(":")
94
+ found = search_paths.map { |path|
95
+ abs = File.expand_path(name, path)
96
+ FileTest.exist?(abs) ? abs : nil
97
+ }
98
+ found.compact[0]
99
+ end
100
+ module_function :search_in_path
101
+
102
+ def add_extension(basename)
103
+ "#{basename}.md"
104
+ end
105
+ module_function :add_extension
106
+ end
107
+ end
@@ -0,0 +1,4 @@
1
+ module Rbnotes
2
+ VERSION = "0.2.1"
3
+ RELEASE = '2020-10-25'
4
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'lib/rbnotes/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rbnotes"
5
+ spec.version = Rbnotes::VERSION
6
+ spec.authors = ["mnbi"]
7
+ spec.email = ["mnbi@users.noreply.github.com"]
8
+
9
+ spec.summary = %q{A simple utility to write a note.}
10
+ spec.description = %q{Rbnotes allows you to write a note into a single repository.}
11
+ spec.homepage = "https://github.com/mnbi/rbnotes"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/mnbi/rbnotes"
17
+ spec.metadata["changelog_uri"] = "https://github.com/mnbi/rbnotes/blob/main/CHANGELOG.md"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "textrepo", "~> 0.4"
29
+ spec.add_dependency "unicode-display_width", "~> 1.7"
30
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbnotes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - mnbi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-10-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: textrepo
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: unicode-display_width
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ description: Rbnotes allows you to write a note into a single repository.
42
+ email:
43
+ - mnbi@users.noreply.github.com
44
+ executables:
45
+ - rbnotes
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - ".travis.yml"
51
+ - CHANGELOG.md
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - LICENSE
55
+ - README.md
56
+ - Rakefile
57
+ - bin/console
58
+ - bin/setup
59
+ - exe/rbnotes
60
+ - lib/rbnotes.rb
61
+ - lib/rbnotes/commands.rb
62
+ - lib/rbnotes/commands/add.rb
63
+ - lib/rbnotes/commands/delete.rb
64
+ - lib/rbnotes/commands/import.rb
65
+ - lib/rbnotes/commands/list.rb
66
+ - lib/rbnotes/commands/show.rb
67
+ - lib/rbnotes/commands/update.rb
68
+ - lib/rbnotes/conf.rb
69
+ - lib/rbnotes/error.rb
70
+ - lib/rbnotes/utils.rb
71
+ - lib/rbnotes/version.rb
72
+ - rbnotes.gemspec
73
+ homepage: https://github.com/mnbi/rbnotes
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ homepage_uri: https://github.com/mnbi/rbnotes
78
+ source_code_uri: https://github.com/mnbi/rbnotes
79
+ changelog_uri: https://github.com/mnbi/rbnotes/blob/main/CHANGELOG.md
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.7.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.1.4
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: A simple utility to write a note.
99
+ test_files: []