topicz 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +7 -0
  7. data/README.md +109 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +7 -0
  11. data/exe/topicz +12 -0
  12. data/lib/topicz/application.rb +44 -0
  13. data/lib/topicz/command_factory.rb +21 -0
  14. data/lib/topicz/commands/_index.rb +18 -0
  15. data/lib/topicz/commands/alfred_command.rb +95 -0
  16. data/lib/topicz/commands/create_command.rb +48 -0
  17. data/lib/topicz/commands/editor_command.rb +14 -0
  18. data/lib/topicz/commands/help_command.rb +31 -0
  19. data/lib/topicz/commands/init_command.rb +61 -0
  20. data/lib/topicz/commands/journal_command.rb +64 -0
  21. data/lib/topicz/commands/note_command.rb +68 -0
  22. data/lib/topicz/commands/path_command.rb +39 -0
  23. data/lib/topicz/commands/report_command.rb +50 -0
  24. data/lib/topicz/commands/repository_command.rb +61 -0
  25. data/lib/topicz/defaults.rb +12 -0
  26. data/lib/topicz/repository.rb +123 -0
  27. data/lib/topicz/version.rb +3 -0
  28. data/topicz.gemspec +27 -0
  29. data/vendor/cache/diff-lcs-1.2.5.gem +0 -0
  30. data/vendor/cache/docile-1.1.5.gem +0 -0
  31. data/vendor/cache/fakefs-0.8.1.gem +0 -0
  32. data/vendor/cache/json-1.8.3.gem +0 -0
  33. data/vendor/cache/rake-10.5.0.gem +0 -0
  34. data/vendor/cache/rspec-3.3.0.gem +0 -0
  35. data/vendor/cache/rspec-core-3.3.2.gem +0 -0
  36. data/vendor/cache/rspec-expectations-3.3.1.gem +0 -0
  37. data/vendor/cache/rspec-mocks-3.3.2.gem +0 -0
  38. data/vendor/cache/rspec-support-3.3.0.gem +0 -0
  39. data/vendor/cache/simplecov-0.11.2.gem +0 -0
  40. data/vendor/cache/simplecov-html-0.10.0.gem +0 -0
  41. data/vendor/cache/zaru-0.1.0.gem +0 -0
  42. metadata +170 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a22e7da57e83490f3607152af595bec565270200
4
+ data.tar.gz: 52a68b28cf5a622b7444ee0170ae45db753610ff
5
+ SHA512:
6
+ metadata.gz: 1ad1e9d6067d868e516c681c35cdf68c60ee00933b59ebbf1eb03a4df6154ba05a2200ed85e4450edd4d199f366d17ad183e31bc01e90bd1fb0c2606e3c4756f
7
+ data.tar.gz: 22716d00ed11955671a772d47d76871b5f99bf7fd46ebfbbb792b6a4c569daa991a06150a5391a91fc42b6c5ae1b8b036b04d89e288fbb50e3a4e60115e15c1f
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea/
11
+ *.iml
12
+ /bundle
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in topicz.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2016 Vincent Oostindië
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Topicz
2
+
3
+ Topicz is a simple topic repository administration tool, where the repository is a regular filesystem.
4
+
5
+ ## Installation
6
+
7
+ Install it as:
8
+
9
+ $ gem install topicz
10
+
11
+ See below (*Development*) in case you’re working with a Git clone directly.
12
+
13
+ ## Usage
14
+
15
+ $ topicz —help
16
+ Usage: topicz [options] <command> [options]
17
+ -c, --config FILE Uses FILE as the configuration file
18
+
19
+ Where <command> is one of:
20
+
21
+ init : Initializes a new topic repository
22
+ create : Creates a new topic
23
+ path : Prints the full path to a topic
24
+ journal: Opens a (new) weekly journal entry for a topic
25
+ note : Opens a new note for a topic
26
+ alfred : Searches in Alfred Script Filter format
27
+ report : Generates a weekly report of all topics
28
+ help : Shows help about a command
29
+
30
+ ## Background
31
+
32
+ In my daily work I have to deal with a lot of different topics. New topics pop up often and existing topics are in varying degree of completeness. Sometimes nothing happens to a topic for weeks. Sometimes topics are short-lived, sometimes they stick around for years. All in all, keeping track of all the topics I work on is a big challenge for me.
33
+
34
+ The way my mind works: if I can’t find a structure to organize my work in, I feel stressed. But like everybody else I prefer to be relaxed. Also I don’t like proprietary formats or tools that force me in a specific way of working.
35
+
36
+ So I created my own structure, completely filesystem based, and scripts on top of it to integrate it with the other tools I use on my Mac, like [Alfred](http://www.alfredapp.com). Topicz collects all those scripts into a consistent, single command-line interface that I can easily change and extend whenever I need.
37
+
38
+ ## The topic repository
39
+
40
+ A topic repository is nothing more than a directory on disk. This directory has subdirectories, one for each topic. The name of the subdirectory is what you like it to be. A topic, in turn, holds a number of directories. Here’s a basic layout:
41
+
42
+ . (topic root)
43
+ ├── Topic 1
44
+ │   ├── topic.yaml
45
+ │   ├── Documents
46
+ │   ├── Journal
47
+ │   ├── Logs
48
+ │   └── Reference Material
49
+ ├── Topic 2
50
+ ├── Topic …
51
+ └── Topic n
52
+
53
+ The `topic.yaml` file is entirely optional. More information on that later.
54
+
55
+ A topic can certainly have other subdirectories than the ones shown above. These four are the ones I always use. Where needed I add more. For example when I go to a conference, I typically add the directories `Travel` and `Bills`.
56
+
57
+ ### Documents and Reference Material
58
+
59
+ The `Documents` and `Reference Material` directories speak for themselves. The first is for stuff I create, the second for stuff that others create. There are no rules as to the files and subdirectories in these directories, although I tend to prefix each filename with the date I created or received the file in `YYYY-mm-DD` format. But that’s just my way of keeping things organized.
60
+
61
+ ### Journal
62
+
63
+ The `Journal` directory is more interesting. It contains Markdown files only, and each filename follows the pattern `<year>-week-<week>.md`. If there was some kind of progress on a topic in a certain week, I describe it here. Every week I send a progress report to my colleagues about all the topics I worked on. Topicz’s `report` command creates this report for me automatically from the separate journal entries. And of course Topicz also helps me creating and opening the right journal files, depending on the topic I work on.
64
+
65
+ ### Notes
66
+
67
+ The `Notes` directory is for meeting notes, scratchpads, reminders, whatever. Every note is a Markdown file and each filename follows the pattern `<YYYY-mm-DD> <subject>.md`. Again Topicz helps me to quickly create and open notes for the topic I’m working on.
68
+
69
+ ## Repository configuration
70
+
71
+ Topicz needs to know just one thing: where your repository is. You stick this into a configuration file, and tell Topicz about this file using the `-c` flag. If you have one topic repository, you can also store this file as `~/.topiczrc`. Then you don’t need to specify it when using the CLI.
72
+
73
+ The first time you use Topicz, just do this:
74
+
75
+ $ topics init /path/to/my/topic/repository
76
+
77
+ Topicz will do two things. First it will create an empty directory at the location you specified. Secondly it will write this location into `~/.topiczrc`.
78
+
79
+ Some commands call an external text editor, for example to edit journals and notes. By default Topicz will use the `EDITOR` environment variable. You can override it on a per-repository basis by defining an `editor` property in the configuration file.
80
+
81
+ ## Topic configuration
82
+
83
+ Each topic directory can have a `topic.yaml` file in it, which holds a YAML file describing that topic. It looks as follows:
84
+
85
+ title: <topic title>
86
+ id: <topic ID>
87
+
88
+ If the YAML file is missing, or a field in the file is missing, then Topicz falls back on sensible defaults: the title is set to the name of the topic directory, and the ID is generated from this name by lowercasing it, replacing whitespace with hyphens, and removing strange characters.
89
+
90
+ > This file is where I’m planning to add lots of functionality to. For example to store topic relationships, or to store metadata. See *Plans* below.
91
+
92
+ You’re free to edit this file at any time, although Topicz also provides commands to help you do this. You never actually need to edit these YAML files themselves, if you don’t want to.
93
+
94
+ ## Development
95
+
96
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
97
+
98
+ 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).
99
+
100
+ ## Plans
101
+
102
+ * Allow topics to be archived when no longer relevant.
103
+ * Allow relationships between topics to be defined in the YAML files, and use these to generate graphs for Graphviz: `depends-on`, `part-of`, `relates-to`. The CLI will help to guarantee correctness, for example when archiving a topic, or when renaming one’s title and/or ID.
104
+ * Allow metadata to be defined for each topic, like main stakeholders, categories and topic goals.
105
+
106
+ ## Contributing
107
+
108
+ Bug reports and pull requests are welcome on GitHub at https://github.com/voostindie/topicz.
109
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "topicz"
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
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/exe/topicz ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'topicz/application'
4
+
5
+ begin
6
+ Topicz::Application.new.run
7
+ rescue Exception => e
8
+ if e.message != 'exit' # There's probably a better way to do this...
9
+ puts e.message
10
+ exit -1
11
+ end
12
+ end
@@ -0,0 +1,44 @@
1
+ require 'topicz/version'
2
+ require 'topicz/command_factory'
3
+ require 'optparse'
4
+ require 'yaml'
5
+
6
+ module Topicz
7
+
8
+ class Application
9
+
10
+ attr_reader :command
11
+
12
+ def initialize(arguments = ARGV, factory = CommandFactory.new)
13
+ config_file = nil
14
+ OptionParser.new do |options|
15
+ options.banner = 'Usage: topicz [options] <command> [options]'
16
+ options.program_name = 'topicz'
17
+ options.version = Topicz::VERSION
18
+ options.on('-c', '--config FILE', 'Uses FILE as the configuration file') do |file|
19
+ config_file = file
20
+ end
21
+ options.separator ''
22
+ options.separator 'Where <command> is one of: '
23
+ options.separator ''
24
+ options.separator Topicz::COMMANDS.to_s
25
+ end.order! arguments
26
+
27
+ unless arguments.empty?
28
+ command = arguments.shift
29
+ if Topicz::COMMANDS.has_key? command
30
+ @command = factory.create_command(command, config_file, arguments)
31
+ end
32
+ end
33
+ end
34
+
35
+ def run
36
+ if @command != nil
37
+ @command.execute
38
+ else
39
+ raise 'Invalid command. Try topicz --help'
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ require 'topicz/commands/_index'
2
+
3
+ module Topicz
4
+
5
+ class CommandFactory
6
+
7
+ def load_command(name)
8
+ unless COMMANDS.has_key?name
9
+ raise "Unsupported command: #{name}"
10
+ end
11
+ require "topicz/commands/#{name}_command"
12
+ Object.const_get("Topicz::Commands::#{name.capitalize}Command")
13
+ end
14
+
15
+ def create_command(name, config_file = nil, arguments = [])
16
+ load_command(name).new(config_file, arguments)
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,18 @@
1
+ module Topicz
2
+
3
+ COMMANDS = {
4
+ 'init' => 'Initializes a new topic repository',
5
+ 'create' => 'Creates a new topic',
6
+ 'path' => 'Prints the full path to a topic',
7
+ 'journal' => 'Opens a (new) weekly journal entry for a topic',
8
+ 'note' => 'Opens a new note for a topic',
9
+ 'alfred' => 'Searches in Alfred Script Filter format',
10
+ 'report' => 'Generates a weekly report of all topics',
11
+ 'help' => 'Shows help about a command',
12
+ }
13
+
14
+ def COMMANDS.to_s
15
+ COMMANDS.collect { |command, description| " #{command.ljust(7)}: #{description}" }.join("\n")
16
+ end
17
+
18
+ end
@@ -0,0 +1,95 @@
1
+ require_relative 'repository_command'
2
+ require 'json'
3
+
4
+ module Topicz::Commands
5
+
6
+ class AlfredCommand < RepositoryCommand
7
+
8
+ def initialize(config_file = nil, arguments = [])
9
+ super(config_file)
10
+ @identifiers = false
11
+ @mode = :browse
12
+ option_parser.order! arguments
13
+ @filter = arguments.join ' '
14
+ end
15
+
16
+ def option_parser
17
+ OptionParser.new do |options|
18
+ options.banner = 'Usage: alfred <filter>'
19
+ options.on('-m', '--mode MODE', 'Set the output mode') do |mode|
20
+ case mode.strip.downcase
21
+ when 'browse' then
22
+ @mode = :browse
23
+ @identifiers = false
24
+ when 'journal' then
25
+ @mode = :journal
26
+ @identifiers = true
27
+ when 'note' then
28
+ @mode = :note
29
+ @identifiers = true
30
+ else
31
+ raise "Invalid mode: '#{mode}'"
32
+ end
33
+ end
34
+ options.separator ''
35
+ options.separator 'Searches for topics and produces the result in JSON for Alfred\'s Script Filter'
36
+ options.separator ''
37
+ options.separator 'The filter specifies the text to search on. The text is matched against the topic\'s: '
38
+ options.separator '- path on the filesystem'
39
+ options.separator '- id, if specified in the topic\'s topic.yaml file'
40
+ options.separator '- title, if specified in the topic\'s topic.yaml file'
41
+ options.separator '- aliases, if specified in the topic\'s topic.yaml file'
42
+ options.separator ''
43
+ options.separator 'Supported modes are:'
44
+ options.separator ' browse : For browsing through topics'
45
+ options.separator ' journal : For creating journal entries'
46
+ options.separator ' note : For creating notes'
47
+ options.separator ''
48
+ options.separator 'Alfred automatically orders the items in its UI based on your usage.'
49
+ end
50
+ end
51
+
52
+ def execute
53
+ topics = @repository.find_all @filter
54
+ items = []
55
+ topics.each do |topic|
56
+ item = {
57
+ # https://www.alfredapp.com/help/workflows/inputs/script-filter/json/
58
+ uid: topic.id, # Affects the ordering in Alfred, based on learning
59
+ type: 'file',
60
+ title: topic.title,
61
+ subtitle: create_subtitle(topic),
62
+ arg: @identifiers ? topic.id : topic.fullpath,
63
+ icon: {
64
+ type: 'fileicon',
65
+ path: topic.fullpath
66
+ }
67
+ }
68
+ if @mode == :browse
69
+ item['mods'] = {
70
+ cmd: {
71
+ subtitle: "Open topic '#{topic.title}' in Finder"
72
+ },
73
+ alt: {
74
+ subtitle: "Open topic '#{topic.title}' in editor"
75
+ }
76
+ }
77
+ end
78
+ items << item
79
+ end
80
+ puts ({items: items}.to_json)
81
+ end
82
+
83
+ def create_subtitle(topic)
84
+ case @mode
85
+ when :browse then
86
+ "Browse topic '#{topic.title}' in Alfred Browser"
87
+ when :journal then
88
+ "Open this weeks journal entry for topic '#{topic.title}'"
89
+ when :note then
90
+ "Create a new note for topic '#{topic.title}'"
91
+ end
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'repository_command'
2
+ require 'topicz/defaults'
3
+ require 'json'
4
+ require 'zaru'
5
+ require 'fileutils'
6
+
7
+ module Topicz::Commands
8
+
9
+ class CreateCommand < RepositoryCommand
10
+
11
+ def initialize(config_file = nil, arguments = [])
12
+ super(config_file)
13
+ @title = arguments.join(' ').strip
14
+ end
15
+
16
+ def option_parser
17
+ OptionParser.new do |options|
18
+ options.banner = 'Usage: create <name>'
19
+ options.separator ''
20
+ options.separator 'Creates a new topic with the specified name'
21
+ options.separator ''
22
+ options.separator 'Basically all this command does is create a new directory structure to hold a topic.'
23
+ options.separator ''
24
+ options.separator 'It is an error if a topic with the specified name already exists.'
25
+ end
26
+ end
27
+
28
+ def execute
29
+ if @repository.exist_title?(@title)
30
+ raise "Topic already exists: '#{@title}'."
31
+ end
32
+ path = Zaru.sanitize! @title
33
+ fullpath = File.join(@repository.root, path)
34
+ if File.exist?(fullpath)
35
+ raise "Directory already exists: '#{path}'."
36
+ end
37
+ FileUtils.mkdir fullpath
38
+ Topicz::DIRECTORIES.each { |dir| FileUtils.mkdir(File.join(fullpath, dir)) }
39
+ if path != @title
40
+ File.open(File.join(fullpath, Topicz::TOPIC_FILE), 'w') do | file |
41
+ file.write(YAML.dump({'title' => @title}))
42
+ end
43
+ end
44
+ puts "Created topic '#{@title}'"
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'repository_command'
2
+
3
+ module Topicz::Commands
4
+
5
+ class EditorCommand < RepositoryCommand
6
+
7
+ def editor
8
+ @config['editor'] ||
9
+ ENV['EDITOR'] ||
10
+ raise('No editor configured. Set one in the topicz configuraton file, or in the EDITOR environment variable.')
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ require 'topicz/command_factory'
2
+
3
+ module Topicz::Commands
4
+
5
+ class HelpCommand
6
+
7
+ def initialize(config_file = nil, arguments = [])
8
+ option_parser.order! arguments
9
+ @help =
10
+ if arguments == nil || arguments.empty?
11
+ self
12
+ else
13
+ Topicz::CommandFactory.new.load_command(arguments.shift).new
14
+ end.option_parser
15
+ end
16
+
17
+ def option_parser
18
+ OptionParser.new do |options|
19
+ options.banner = 'Usage: help <command>'
20
+ options.separator ''
21
+ options.separator 'Shows help about a specific command. Valid commands are:'
22
+ options.separator ''
23
+ options.separator Topicz::COMMANDS.to_s
24
+ end
25
+ end
26
+
27
+ def execute
28
+ puts @help.help
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,61 @@
1
+ require 'topicz/defaults'
2
+ require 'fileutils'
3
+ require 'optparse'
4
+ require 'yaml'
5
+
6
+ module Topicz::Commands
7
+
8
+ class InitCommand
9
+
10
+ def initialize(config_file = nil, arguments = [])
11
+ @config_file = Topicz::DEFAULT_CONFIG_LOCATION
12
+ option_parser.order! arguments
13
+ unless arguments.empty?
14
+ @directory = arguments.shift
15
+ end
16
+ end
17
+
18
+ def option_parser
19
+ OptionParser.new do |options|
20
+ options.banner = 'Usage: init [options] <directory>'
21
+ options.on('-c', '--config FILE') do |file|
22
+ @config_file = file
23
+ end
24
+ options.separator ''
25
+ options.separator 'Initializes a new topic repository. This creates a directory and writes its
26
+ location into a configuration file.'
27
+ options.separator ''
28
+ options.separator 'If the directory already exists, this command fails.'
29
+ options.separator ''
30
+ options.separator 'If a configuration file already exists, it will not be overwritten.'
31
+ end
32
+ end
33
+
34
+ def execute
35
+ unless @directory
36
+ raise 'Pass the location of the new repository as an argument.'
37
+ end
38
+ if File.exist? @directory
39
+ raise "A file or directory already exists at this location: #{@directory}."
40
+ end
41
+ create_repository
42
+ create_configuration
43
+ end
44
+
45
+ def create_repository
46
+ FileUtils.mkdir_p(@directory)
47
+ puts "New topic repository created at: #{@directory}."
48
+ end
49
+
50
+ def create_configuration
51
+ if File.exist? @config_file
52
+ puts "Skipping creation of configuration file; one already exists at #{@config_file}."
53
+ else
54
+ File.open(@config_file, 'w') do |file|
55
+ file.write(YAML.dump({'repository' => @directory}))
56
+ end
57
+ puts "Configuration file saved to: #{@config_file}."
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,64 @@
1
+ require_relative 'editor_command'
2
+ require 'topicz/defaults'
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module Topicz::Commands
7
+
8
+ class JournalCommand < EditorCommand
9
+
10
+ def initialize(config_file = nil, arguments = [], kernel = Kernel)
11
+ super(config_file)
12
+ @kernel = kernel
13
+ @strict = false
14
+ @week = Date.today.cweek
15
+ @year = Date.today.cwyear
16
+ option_parser.order! arguments
17
+ @filter = arguments.join ' '
18
+ end
19
+
20
+ def option_parser
21
+ OptionParser.new do |options|
22
+ options.banner = 'Usage: journal <filter>'
23
+ options.on('-s', '--strict', 'Do a full strict match on topic IDs only') do
24
+ @strict = true
25
+ end
26
+ options.on('-w', '--week WEEK', 'Use week WEEK instead of the current week') do |week|
27
+ @week = week.to_i
28
+ end
29
+ options.on('-y', '--year YEAR', 'Use year YEAR instead of the current year') do |year|
30
+ @year = year.to_i
31
+ end
32
+ options.separator ''
33
+ options.separator 'Opens the weekly journal for the specified topic.'
34
+ options.separator ''
35
+ options.separator 'The filter specifies the text to search on. The text is matched against the topic\'s: '
36
+ options.separator '- path on the filesystem'
37
+ options.separator '- id, if specified in the topic\'s topic.yaml file'
38
+ options.separator '- title, if specified in the topic\'s topic.yaml file'
39
+ options.separator '- aliases, if specified in the topic\'s topic.yaml file'
40
+ options.separator ''
41
+ options.separator 'The filter must return precisely one topic. Zero or more matches give an error.'
42
+ end
43
+ end
44
+
45
+ def execute
46
+ topic = find_exactly_one_topic(@filter, @strict)
47
+ path = File.join(topic.fullpath, Topicz::DIR_JOURNAL)
48
+ FileUtils.mkdir(path) unless Dir.exist? path
49
+
50
+ year = @year.to_s
51
+ week = @week.to_s.rjust(2, '0')
52
+ path = File.join(path, "#{year}-week-#{week}.md")
53
+
54
+ unless File.exists? path
55
+ File.open(path, 'w') do |file|
56
+ file.puts("# #{topic.title} - Week #{week}, #{year}")
57
+ end
58
+ end
59
+
60
+ @kernel.exec "#{editor} \"#{path}\""
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'editor_command'
2
+ require 'topicz/defaults'
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'zaru'
6
+
7
+ module Topicz::Commands
8
+
9
+ class NoteCommand < EditorCommand
10
+
11
+ def initialize(config_file = nil, arguments = [], kernel = Kernel)
12
+ super(config_file)
13
+ @kernel = kernel
14
+ @strict = false
15
+ option_parser.order! arguments
16
+ @filter = arguments.shift
17
+ @title = arguments.empty? ? nil : arguments.join(' ')
18
+ end
19
+
20
+ def option_parser
21
+ OptionParser.new do |options|
22
+ options.banner = 'Usage: note <filter> [<title>]'
23
+ options.on('-s', '--strict', 'Do a full strict match on topic IDs only') do
24
+ @strict = true
25
+ end
26
+ options.separator ''
27
+ options.separator 'Creates a new note for the specified topic.'
28
+ options.separator ''
29
+ options.separator 'The filter specifies the text to search on. The text is matched against the topic\'s: '
30
+ options.separator '- path on the filesystem'
31
+ options.separator '- id, if specified in the topic\'s topic.yaml file'
32
+ options.separator '- title, if specified in the topic\'s topic.yaml file'
33
+ options.separator '- aliases, if specified in the topic\'s topic.yaml file'
34
+ options.separator ''
35
+ options.separator 'The filter must return precisely one topic. Zero or more matches give an error.'
36
+ options.separator ''
37
+ options.separator 'The note title is optional. If omitted the title will be \'Unnamed note\'.'
38
+ end
39
+ end
40
+
41
+ def execute
42
+ topic = find_exactly_one_topic(@filter, @strict)
43
+ path = File.join(topic.fullpath, Topicz::DIR_NOTES)
44
+ FileUtils.mkdir(path) unless Dir.exist? path
45
+
46
+ if @title
47
+ date = DateTime.now.strftime('%Y-%m-%d')
48
+ title = @title
49
+ filename = Zaru.sanitize! "#{date} #{title}.md"
50
+ else
51
+ date = DateTime.now.strftime('%Y-%m-%d %H%M')
52
+ title = 'Unnamed note'
53
+ filename = "#{date}.md"
54
+ end
55
+
56
+ path = File.join(path, filename)
57
+
58
+ unless File.exists? path
59
+ File.open(path, 'w') do | file |
60
+ file.puts("# #{topic.title} - #{title}")
61
+ end
62
+ end
63
+
64
+ @kernel.exec "#{editor} \"#{path}\""
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'repository_command'
2
+ require 'json'
3
+
4
+ module Topicz::Commands
5
+
6
+ class PathCommand < RepositoryCommand
7
+
8
+ def initialize(config_file = nil, arguments = [])
9
+ super(config_file)
10
+ @strict = false
11
+ option_parser.order! arguments
12
+ @filter = arguments.join ' '
13
+ end
14
+
15
+ def option_parser
16
+ OptionParser.new do |options|
17
+ options.banner = 'Usage: path <filter>'
18
+ options.on('-s', '--strict', 'Do a full strict match on topic IDs only') do
19
+ @strict = true
20
+ end
21
+ options.separator ''
22
+ options.separator 'Prints the absolute path to the topic that matches <filter>.'
23
+ options.separator ''
24
+ options.separator 'The filter specifies the text to search on. The text is matched against the topic\'s: '
25
+ options.separator '- path on the filesystem'
26
+ options.separator '- id, if specified in the topic\'s topic.yaml file'
27
+ options.separator '- title, if specified in the topic\'s topic.yaml file'
28
+ options.separator '- aliases, if specified in the topic\'s topic.yaml file'
29
+ options.separator ''
30
+ options.separator 'The filter must return precisely one topic. Zero or more matches give an error.'
31
+ end
32
+ end
33
+
34
+ def execute
35
+ print find_exactly_one_topic(@filter, @strict).fullpath
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ require_relative 'repository_command'
2
+ require 'json'
3
+
4
+ module Topicz::Commands
5
+
6
+ class ReportCommand < RepositoryCommand
7
+
8
+ def initialize(config_file = nil, arguments = [])
9
+ super(config_file)
10
+ @week = Date.today.cweek
11
+ @year = Date.today.cwyear
12
+ option_parser.order! arguments
13
+ @filter = arguments.join ' '
14
+ end
15
+
16
+ def option_parser
17
+ OptionParser.new do |options|
18
+ options.banner = 'Usage: report'
19
+ options.on('-w', '--week WEEK', 'Use week WEEK instead of the current week') do |week|
20
+ @week = week.to_i
21
+ end
22
+ options.on('-y', '--year YEAR', 'Use year YEAR instead of the current year') do |year|
23
+ @year = year.to_i
24
+ end
25
+ options.separator ''
26
+ options.separator 'Generates a weekly report from all journals across all topics.'
27
+ end
28
+ end
29
+
30
+ def execute
31
+ year = @year.to_s
32
+ week = @week.to_s.rjust(2, '0')
33
+ path = File.join(Topicz::DIR_JOURNAL, "#{year}-week-#{week}.md")
34
+
35
+ @repository.topics.each do |topic|
36
+ journal = File.join(topic.fullpath, path)
37
+ next unless File.exist? journal
38
+
39
+ puts "## #{topic.title}"
40
+ puts
41
+ puts File.readlines(journal)
42
+ .drop(2).join() # Drop first 2 lines: title (week) and empty line
43
+ .gsub(/^#(.*)/, '##\1') # Add an extra '#' in front of every title
44
+ .strip # Remove leading and ending whitespace
45
+ puts
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,61 @@
1
+ require 'topicz/defaults'
2
+ require 'topicz/repository'
3
+ require 'yaml'
4
+
5
+ module Topicz::Commands
6
+
7
+ class RepositoryCommand
8
+
9
+ def initialize(config_file = nil)
10
+ @config = load_config(config_file)
11
+ @repository = load_repository
12
+ end
13
+
14
+ def load_config(config_file)
15
+ file = config_file != nil ? config_file : Topicz::DEFAULT_CONFIG_LOCATION
16
+ unless File.exist? file
17
+ raise "File doesn't exist: #{file}."
18
+ end
19
+ unless File.readable? file
20
+ raise "File isn't readable: #{file}."
21
+ end
22
+ begin
23
+ config = YAML.load_file(file)
24
+ rescue
25
+ raise "Not a valid YAML file: #{file}."
26
+ end
27
+ unless config.has_key?('repository')
28
+ raise "Missing required property 'repository' in configuration file: #{file}."
29
+ end
30
+ config
31
+ end
32
+
33
+ def load_repository
34
+ directory = @config['repository']
35
+ unless Dir.exist? directory
36
+ raise "Repository directory doesn't exist: #{directory}."
37
+ end
38
+ Topicz::Repository.new(directory)
39
+ end
40
+
41
+ def find_exactly_one_topic(filter, strict)
42
+ if strict
43
+ topic = @repository[filter]
44
+ if topic == nil
45
+ raise "No topic found with ID: '#{filter}'"
46
+ end
47
+ topic
48
+ else
49
+ topics = @repository.find_all filter
50
+ if topics.length == 0
51
+ raise "No topics found matching the search filter: '#{filter}'"
52
+ end
53
+ if topics.length > 1
54
+ matches = topics.map { |t| t.title }.join("\n")
55
+ raise "Multiple topics match the search filter: '#{filter}'. Matches:\n#{matches}"
56
+ end
57
+ topics[0]
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+ module Topicz
2
+ DEFAULT_CONFIG_LOCATION = File.join(Dir.home, '.topiczrc')
3
+
4
+ TOPIC_FILE = 'topic.yaml'
5
+
6
+ DIR_NOTES = 'Notes'
7
+ DIR_JOURNAL = 'Journal'
8
+ DIR_REFERENCE = 'Reference'
9
+ DIR_DOCUMENTS = 'Documents'
10
+
11
+ DIRECTORIES = [DIR_NOTES, DIR_JOURNAL, DIR_REFERENCE, DIR_DOCUMENTS]
12
+ end
@@ -0,0 +1,123 @@
1
+ require 'yaml'
2
+
3
+ module Topicz
4
+
5
+ class Repository
6
+
7
+ attr_reader :root
8
+
9
+ def initialize(root)
10
+ @root = root
11
+ @topics = {}
12
+ errors = []
13
+ Dir.foreach(root) do |path|
14
+ next if path.start_with?('.')
15
+ next unless File.directory?(File.join(root, path))
16
+ topic = Topic.new(root, path)
17
+ if @topics.has_key?(topic.id)
18
+ errors << "Error in topic '#{topic.title}': ID '#{topic.id}' is already in use."
19
+ end
20
+ @topics[topic.id] = topic
21
+ end
22
+ @topics.each_value do |topic|
23
+ topic.references.each do |ref|
24
+ unless @topics.has_key?(ref)
25
+ errors << "Error in topic '#{topic.title}': Reference to non-existing topic ID '#{ref}'."
26
+ end
27
+ end
28
+ end
29
+ unless errors.empty?
30
+ raise errors
31
+ end
32
+ end
33
+
34
+ def [](id)
35
+ @topics[id]
36
+ end
37
+
38
+ def exist_title?(title)
39
+ @topics.values.any? { |t| t.title == title }
40
+ end
41
+
42
+ def topics
43
+ @topics.values
44
+ end
45
+
46
+ def find_all(filter = nil)
47
+ @topics.values.select { |t| t.matches(filter) }
48
+ end
49
+
50
+ end
51
+
52
+ class Topic
53
+
54
+ attr_reader :id, :path, :title, :category, :aliases, :metadata
55
+
56
+ def initialize(root, path)
57
+ @root = root
58
+ @path = path
59
+
60
+ descriptor = File.join(@root, @path, 'topic.yaml')
61
+ yaml = if File.exist?(descriptor)
62
+ YAML.load_file(descriptor)
63
+ else
64
+ {}
65
+ end
66
+
67
+ @id = yaml['id'] || create_id(path)
68
+ @category = yaml['category'] || 'none'
69
+ @title = yaml['title'] || @path
70
+ @aliases = yaml['aliases'] || []
71
+ @parents = yaml['depends on'] || {}
72
+ @relations = yaml['relates to'] || {}
73
+ @metadata = yaml['metadata'] || {}
74
+ end
75
+
76
+ # Checks whether this topic's title or one of its aliases matches the filter
77
+ # The filter may be `nil`, in which case it is said to match.
78
+ def matches(filter)
79
+ return true unless filter
80
+ filter = filter.downcase
81
+ @title.downcase.include?(filter) ||
82
+ @id.downcase.include?(filter) ||
83
+ !(@aliases.select { |a| a.downcase.include?(filter) }.empty?) ||
84
+ @path.downcase.include?(filter)
85
+ end
86
+
87
+ # Full path to this topic on disk
88
+ def fullpath
89
+ File.join(@root, @path)
90
+ end
91
+
92
+ # List of unique topic IDs that this topic refers to.
93
+ def references
94
+ @parents.keys
95
+ end
96
+
97
+ def parents
98
+ @parents.keys
99
+ end
100
+
101
+ def relations
102
+ @relations.keys
103
+ end
104
+
105
+ def to_s
106
+ <<-EOS
107
+ Topic '#{@id}' {
108
+ title: '#{@title}',
109
+ aliases: '#{@aliases}',
110
+ category: '#{@category}',
111
+ parents: #{@parents},
112
+ relations: #{@relations}
113
+ }
114
+ EOS
115
+ end
116
+
117
+ private
118
+ def create_id(path)
119
+ path.downcase.gsub(/[\_,;\.\&]/, '').gsub(/[ ]/, '-').gsub(/\-\-/, '-')
120
+ end
121
+ end
122
+
123
+ end
@@ -0,0 +1,3 @@
1
+ module Topicz
2
+ VERSION = "0.1.0"
3
+ end
data/topicz.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'topicz/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "topicz"
8
+ spec.version = Topicz::VERSION
9
+ spec.authors = ["Vincent Oostindië"]
10
+ spec.email = ["vincent@ulso.nl"]
11
+
12
+ spec.summary = %q{Filesystem based topic administration tool}
13
+ spec.description = %q{Topicz is a filesystem based topic administration tool. All it does is manipulate files and directories. But it does so in a structured way, helping you to keep track of your own documents, reference material, and topic relationships.}
14
+ spec.homepage = "https://github.com/voostindie/topicz"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'zaru', '~> 0.1.0'
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.3.0" # this one works with the IntelliJ Rake Runner plugin
25
+ spec.add_development_dependency "simplecov", "~> 0.11.2"
26
+ spec.add_development_dependency "fakefs", "~> 0.8.1"
27
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: topicz
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vincent Oostindië
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: zaru
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.3.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.3.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.11.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.11.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: fakefs
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.8.1
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.8.1
97
+ description: Topicz is a filesystem based topic administration tool. All it does is
98
+ manipulate files and directories. But it does so in a structured way, helping you
99
+ to keep track of your own documents, reference material, and topic relationships.
100
+ email:
101
+ - vincent@ulso.nl
102
+ executables:
103
+ - topicz
104
+ extensions: []
105
+ extra_rdoc_files: []
106
+ files:
107
+ - ".gitignore"
108
+ - ".rspec"
109
+ - ".travis.yml"
110
+ - Gemfile
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - bin/console
115
+ - bin/setup
116
+ - exe/topicz
117
+ - lib/topicz/application.rb
118
+ - lib/topicz/command_factory.rb
119
+ - lib/topicz/commands/_index.rb
120
+ - lib/topicz/commands/alfred_command.rb
121
+ - lib/topicz/commands/create_command.rb
122
+ - lib/topicz/commands/editor_command.rb
123
+ - lib/topicz/commands/help_command.rb
124
+ - lib/topicz/commands/init_command.rb
125
+ - lib/topicz/commands/journal_command.rb
126
+ - lib/topicz/commands/note_command.rb
127
+ - lib/topicz/commands/path_command.rb
128
+ - lib/topicz/commands/report_command.rb
129
+ - lib/topicz/commands/repository_command.rb
130
+ - lib/topicz/defaults.rb
131
+ - lib/topicz/repository.rb
132
+ - lib/topicz/version.rb
133
+ - topicz.gemspec
134
+ - vendor/cache/diff-lcs-1.2.5.gem
135
+ - vendor/cache/docile-1.1.5.gem
136
+ - vendor/cache/fakefs-0.8.1.gem
137
+ - vendor/cache/json-1.8.3.gem
138
+ - vendor/cache/rake-10.5.0.gem
139
+ - vendor/cache/rspec-3.3.0.gem
140
+ - vendor/cache/rspec-core-3.3.2.gem
141
+ - vendor/cache/rspec-expectations-3.3.1.gem
142
+ - vendor/cache/rspec-mocks-3.3.2.gem
143
+ - vendor/cache/rspec-support-3.3.0.gem
144
+ - vendor/cache/simplecov-0.11.2.gem
145
+ - vendor/cache/simplecov-html-0.10.0.gem
146
+ - vendor/cache/zaru-0.1.0.gem
147
+ homepage: https://github.com/voostindie/topicz
148
+ licenses: []
149
+ metadata: {}
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project:
166
+ rubygems_version: 2.6.6
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: Filesystem based topic administration tool
170
+ test_files: []