kood 0.0.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,40 @@
1
+ class Kood::CLI < Thor
2
+
3
+ desc "edit [<CARD-ID|CARD-TITLE>]", "Launch the configured editor to modify the card"
4
+ def edit(card_id_or_title = nil)
5
+ Kood::Board.current!.with_context do |current_board|
6
+ card = Kood::Card.find_by_partial_id_or_title!(card_id_or_title)
7
+ success, command = false, ""
8
+
9
+ editor = [ ENV['KOOD_EDITOR'], ENV['EDITOR'] ].find { |e| !e.nil? && !e.empty? }
10
+ return error "To edit a card set $EDITOR or $KOOD_EDITOR." unless editor
11
+
12
+ changed = card.edit_file do |filepath|
13
+ command = "#{ editor } #{ filepath }"
14
+ success = system(command)
15
+ end
16
+
17
+ if not success
18
+ error "Could not run `#{ command }`."
19
+ elsif changed
20
+ ok "Card updated."
21
+ else
22
+ error "The editor exited without changes. Run `kood update` to persist changes."
23
+ end
24
+ end
25
+ end
26
+
27
+ desc "update [<CARD-ID|CARD-TITLE>]", "Persist changes made to cards", hide: true
28
+ def update(card_id_or_title = nil)
29
+ Kood::Board.current!.with_context do |current_board|
30
+ card = Kood::Card.find_by_partial_id_or_title!(card_id_or_title)
31
+ changed = card.edit_file
32
+
33
+ if changed
34
+ ok "Card updated."
35
+ else
36
+ error "No changes to persist."
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,124 @@
1
+ module Kood
2
+ # Utility functions related to the Shell.
3
+ #
4
+ # Contains functions for colors from [git.io/thor](//git.io/thor) and for terminal size
5
+ # from [git.io/hirb](//git.io/hirb). A special Thank You for both authors.
6
+ #
7
+ module Shell
8
+ extend self
9
+
10
+ # Embed in a String to clear all previous ANSI sequences.
11
+ CLEAR = "\e[0m"
12
+ # The start of an ANSI bold sequence.
13
+ BOLD = "\e[1m"
14
+
15
+ # Set the terminal's foreground ANSI color to black.
16
+ BLACK = "\e[30m"
17
+ # Set the terminal's foreground ANSI color to red.
18
+ RED = "\e[31m"
19
+ # Set the terminal's foreground ANSI color to green.
20
+ GREEN = "\e[32m"
21
+ # Set the terminal's foreground ANSI color to yellow.
22
+ YELLOW = "\e[33m"
23
+ # Set the terminal's foreground ANSI color to blue.
24
+ BLUE = "\e[34m"
25
+ # Set the terminal's foreground ANSI color to magenta.
26
+ MAGENTA = "\e[35m"
27
+ # Set the terminal's foreground ANSI color to cyan.
28
+ CYAN = "\e[36m"
29
+ # Set the terminal's foreground ANSI color to white.
30
+ WHITE = "\e[37m"
31
+
32
+ # Set the terminal's background ANSI color to black.
33
+ ON_BLACK = "\e[40m"
34
+ # Set the terminal's background ANSI color to red.
35
+ ON_RED = "\e[41m"
36
+ # Set the terminal's background ANSI color to green.
37
+ ON_GREEN = "\e[42m"
38
+ # Set the terminal's background ANSI color to yellow.
39
+ ON_YELLOW = "\e[43m"
40
+ # Set the terminal's background ANSI color to blue.
41
+ ON_BLUE = "\e[44m"
42
+ # Set the terminal's background ANSI color to magenta.
43
+ ON_MAGENTA = "\e[45m"
44
+ # Set the terminal's background ANSI color to cyan.
45
+ ON_CYAN = "\e[46m"
46
+ # Set the terminal's background ANSI color to white.
47
+ ON_WHITE = "\e[47m"
48
+
49
+ # Set color by using a string or one of the defined constants. If a third
50
+ # option is set to true, it also adds bold to the string. This is based
51
+ # on Highline implementation and it automatically appends CLEAR to the end
52
+ # of the returned String.
53
+ #
54
+ # Pass foreground, background and bold options to this method as
55
+ # symbols.
56
+ #
57
+ # Example:
58
+ #
59
+ # set_color "Hi!", :red, :on_white, :bold
60
+ #
61
+ def set_color(string, *colors)
62
+ ansi_colors = colors.map { |color| lookup_color(color) }
63
+ "#{ansi_colors.join}#{string}#{CLEAR}"
64
+ end
65
+
66
+ # Determines if a shell command exists by searching for it in `ENV['PATH']`.
67
+ # Utility function gently stolen from [git.io/hirb](//git.io/hirb)
68
+ def command_exists?(command)
69
+ ENV['PATH'].split(File::PATH_SEPARATOR).any? {|d| File.exists? File.join(d, command) }
70
+ end
71
+
72
+ # Returns width and height of terminal when detected, nil if not detected.
73
+ # Utility function gently stolen from [git.io/hirb](//git.io/hirb)
74
+ # @return [Array] width, height
75
+ def terminal_size
76
+ if (ENV['COLUMNS'] =~ /^\d+$/) && (ENV['LINES'] =~ /^\d+$/)
77
+ [ENV['COLUMNS'].to_i, ENV['LINES'].to_i]
78
+ elsif (RUBY_PLATFORM =~ /java/ || (!STDIN.tty? && ENV['TERM'])) && command_exists?('tput')
79
+ [`tput cols`.to_i, `tput lines`.to_i]
80
+ elsif STDIN.tty? && command_exists?('stty')
81
+ `stty size`.scan(/\d+/).map { |s| s.to_i }.reverse
82
+ else
83
+ nil
84
+ end
85
+ rescue
86
+ nil
87
+ end
88
+
89
+ # Check if terminal supports unicode characters
90
+ def unicode?
91
+ "\u2501" != 'u2501'
92
+ end
93
+
94
+ # Check if terminal supports colors. Condition stolen from Thor.
95
+ def color_support?
96
+ !(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/) || ENV['ANSICON']
97
+ end
98
+
99
+ # Horizontal delimiter for box drawing
100
+ # @return [String]
101
+ def horizontal_bar
102
+ unicode? ? "\u2501" : '-'
103
+ end
104
+
105
+ # Vertical delimiter for box drawing
106
+ # @return [String]
107
+ def vertical_bar
108
+ unicode? ? "\u2503" : '|'
109
+ end
110
+
111
+ # Try to convert a string to a float or integer. Returns the converted object or the
112
+ # original string if it cannot be converted. Based on code from
113
+ # [stackoverflow.com/a/8072164/543293](//stackoverflow.com/a/8072164/543293)
114
+ def type_cast(v)
115
+ ((float = Float(v)) && (float % 1.0 == 0) ? float.to_i : float) rescue v
116
+ end
117
+
118
+ protected
119
+
120
+ def lookup_color(color)
121
+ self.class.const_get(color.to_s.upcase)
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,195 @@
1
+ require_relative "shell"
2
+
3
+ module Kood
4
+ class Column < Array
5
+ include Shell
6
+
7
+ attr_accessor :width, :rows, :separator
8
+
9
+ def initialize(width)
10
+ @width = width
11
+ @rows = []
12
+ end
13
+
14
+ # Add a new row to this column
15
+ # @param [String] row All contents to be added to the row of the column. It may be
16
+ # split in several lines.
17
+ # @param [Hash] options The `:separator` option adds a horizontal line to the end of
18
+ # the row. The `:align` option is used for text alignment; acceptable values are
19
+ # `ljust`, `lright` and `center`. The `:slice` option slices the given `row`
20
+ # parameter in order to fit in the column width. The following keys with default
21
+ # values will be added if not present: `{ separator: true, align: 'ljust',
22
+ # slice: true }`
23
+ def add_row(row, options = {})
24
+ options = { separator: true, align: 'ljust', slice: true }.merge(options)
25
+ row = row.to_s.force_encoding("UTF-8")
26
+
27
+ if @width and options[:slice]
28
+ sliced_rows = []
29
+ row.split("\n").each do |row|
30
+ sliced_rows += self.slice_row(row, options)
31
+ end
32
+ sliced_rows << self.separator if options[:separator]
33
+ self.add_rows(sliced_rows)
34
+ else
35
+ @rows.push(row)
36
+ end
37
+ end
38
+
39
+ # A sequence of dashes with the same width of the column
40
+ # @return [String]
41
+ def separator
42
+ self.horizontal_bar * @width
43
+ end
44
+
45
+ protected
46
+
47
+ def add_rows(rows)
48
+ @rows.push(*rows)
49
+ end
50
+
51
+ def slice_row(row, options = {})
52
+ sliced_rows = []
53
+ i = 0
54
+
55
+ while slice = row[i, @width]
56
+ break if slice.blank? and !row.blank?
57
+ i += @width
58
+
59
+ # This slice may start with space(s). If it does, remove them, grab some extra
60
+ # characters and add them to the slice
61
+ i, slice = lstrip_row(i, slice, row)
62
+
63
+ # If this slice does not end with a space, and the first character of the next
64
+ # slice is not a space, it means we would be cutting a word in half. If this
65
+ # is not a big word move it to the next line
66
+ i, slice = hyphenize_row(i, slice, row)
67
+
68
+ slice = align_row(slice, options)
69
+ slice = colorize_row(slice, options) if options.key? :color
70
+
71
+ sliced_rows << slice
72
+ end
73
+ sliced_rows
74
+ end
75
+
76
+ def lstrip_row(i, slice, row)
77
+ slice = slice.lstrip
78
+ len_diff = @width - slice.length
79
+ if slice.length < @width and not row[i, len_diff].blank?
80
+ slice += row[i, len_diff]
81
+ i += len_diff
82
+ end
83
+ return i, slice
84
+ end
85
+
86
+ def hyphenize_row(i, slice, row)
87
+ if slice[-1] != ' ' and row[i] and row[i] != ' '
88
+ last_word = slice.split.last
89
+ len_diff = slice.length - last_word.length
90
+ if not last_word.blank? and last_word.length <= (@width * 0.25).floor
91
+ unless slice[0, len_diff].blank?
92
+ slice = slice[0, len_diff]
93
+ i -= last_word.length
94
+ end
95
+ elsif not last_word.blank? # Cut the word and add an hyphen
96
+ slice = slice[0..-2] +"-"
97
+ i -= 1
98
+ end
99
+ end
100
+ return i, slice
101
+ end
102
+
103
+ def align_row(slice, options = {})
104
+ slice.send(options[:align], @width)
105
+ end
106
+
107
+ def colorize_row(slice, options = {})
108
+ options.key?(:color) ? set_color(slice, *options[:color]) : slice
109
+ end
110
+ end
111
+
112
+ class Table
113
+ include Shell
114
+
115
+ attr_accessor :width, :cols, :col_width
116
+
117
+ def initialize(num_columns, width = nil)
118
+ terminal_width = width || terminal_size[0] || 70
119
+ spare_cols = (terminal_width - 3 * num_columns -1) % num_columns
120
+ @width = terminal_width - spare_cols
121
+ @num_cols = num_columns
122
+ @col_width = (@width - 3 * num_columns -1) / num_columns
123
+ @cols = []
124
+ raise "There is not enough space to accommodate all columns" if @col_width < 5
125
+ end
126
+
127
+ def new_column
128
+ raise "Unable to add more columns to table" if @cols.size == @num_cols
129
+ column = Kood::Column.new(@col_width)
130
+ @cols << column
131
+ column
132
+ end
133
+
134
+ def to_s(options = { separator: true })
135
+ max_col_rows = biggest_column.rows
136
+ return empty_row(options) if max_col_rows.length.zero?
137
+
138
+ output = max_col_rows.each_with_index.map do |col_row, i|
139
+ # Don't print the last separator if this is the last thing to be printed
140
+ row(i, options) unless i == max_col_rows.length-1 and col_row == @cols[0].separator
141
+ end.join.chomp
142
+
143
+ improve_cell_corners(output)
144
+ end
145
+
146
+ # This code comes from [git.io/command_line_reporter](//git.io/command_line_reporter).
147
+ # A special Thank You to the authors.
148
+ def separator(type = 'middle')
149
+ if unicode?
150
+ case type
151
+ when 'first' then left, center, right = "\u250F", "\u2533", "\u2513"
152
+ when 'middle' then left, center, right = "\u2523", "\u254A", "\u252B"
153
+ when 'last' then left, center, right = "\u2517", "\u253B", "\u251B"
154
+ end
155
+ else
156
+ left = right = center = '+'
157
+ end
158
+ left + @cols.map { |c| horizontal_bar * (c.width + 2) }.join(center) + right
159
+ end
160
+
161
+ private
162
+
163
+ # Returns the column with the largest number of rows
164
+ def biggest_column
165
+ @cols.max_by { |col| col.rows.length }
166
+ end
167
+
168
+ # Returns an empty (full width) row
169
+ def empty_row(options = { separator: true })
170
+ vbar = options[:separator] ? self.vertical_bar : " "
171
+ @cols.map { |col| vbar + " #{ " "*@col_width } " }.join + vbar
172
+ end
173
+
174
+ # Returns an entire row from the table (which may cross several columns)
175
+ def row(row_index, options = { separator: true })
176
+ vbar = options[:separator] ? self.vertical_bar : " "
177
+ out = @cols.map { |col| vbar + " #{ col.rows[row_index] || " "*@col_width } " }.join
178
+ out + vbar + "\n"
179
+ end
180
+
181
+ # Improves cell corners | |
182
+ # For example, |--|--| becomes |--+--|
183
+ # | |
184
+ def improve_cell_corners(table_str)
185
+ # The ([\e\[\d*m]*) groups are meant to handle colored horizontal bars. This does
186
+ # not take into account vertical colored bars and assumes the color code is
187
+ # positioned before the first horizontal bar or after the last one.
188
+ str = table_str.dup.gsub(/\u2501([\e\[\d*m]*) \u2503 ([\e\[\d*m]*)\u2501/) do
189
+ "\u2501#{ $1 }\u2501\u254B\u2501#{ $2 }\u2501"
190
+ end
191
+ str.gsub!(/\u2501([\e\[\d*m]*) \u2503/) { "\u2501#{ $1 }\u2501\u252B" }
192
+ str.gsub(/\u2503 ([\e\[\d*m]*)\u2501/) { "\u2523\u2501#{ $1 }\u2501" }
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,70 @@
1
+ class Kood::CLI < Thor
2
+
3
+ desc "list [OPTIONS] [<LIST-ID>]", "Display and manage lists"
4
+ #
5
+ # Delete a list. If <list-id> is present, the specified list will be deleted.
6
+ method_option :delete, :aliases => '-d', :type => :boolean
7
+ #
8
+ # Copy a list. <list-id> will be copied to <new-list-id>.
9
+ # <list-id> is kept intact and a new one is created with the exact same data.
10
+ method_option :copy, :aliases => '-c', :type => :string
11
+ #
12
+ # Move a list to another board. <list-id> will be moved to <board-id>.
13
+ method_option :move, :aliases => '-m', :type => :string
14
+ def list(list_id = nil)
15
+ Kood::Board.current!.with_context do |current_board|
16
+ # If no arguments and options are specified, the command displays all existing lists
17
+ if list_id.nil? and no_method_options?
18
+ list_existing_lists(current_board)
19
+
20
+ # If the <list-id> argument is present without options, a new list will be created
21
+ elsif no_method_options?
22
+ create_list(current_board, list_id)
23
+
24
+ # Since <list-id> is present, operate on the specified list
25
+ else
26
+ operate_on_list(current_board, list_id)
27
+ end
28
+ end
29
+ end
30
+ map 'lists' => 'list'
31
+
32
+ private
33
+
34
+ def operate_on_list(current_board, list_id)
35
+ list = Kood::List.get!(list_id)
36
+
37
+ if options.copy.present?
38
+ # TODO
39
+ end # The copied list may be deleted or moved now
40
+
41
+ if options.move.present?
42
+ # TODO
43
+ # If the list was moved, it cannot be deleted
44
+
45
+ elsif options.key? 'delete'
46
+ delete_list(current_board, list_id)
47
+ end
48
+ end
49
+
50
+ def list_existing_lists(current_board)
51
+ error "No lists were found." if current_board.lists.empty?
52
+ puts current_board.list_ids
53
+ end
54
+
55
+ def create_list(current_board, list_id)
56
+ list = current_board.lists.create(id: list_id)
57
+
58
+ if list.persisted?
59
+ ok "List created."
60
+ else
61
+ msgs = list.errors.full_messages.join("\n")
62
+ error "#{ msgs }."
63
+ end
64
+ end
65
+
66
+ def delete_list(current_board, list_id)
67
+ current_board.lists.destroy(list_id)
68
+ ok "List deleted."
69
+ end
70
+ end
@@ -0,0 +1,37 @@
1
+ class Kood::CLI < Thor
2
+
3
+ private
4
+
5
+ # Load third-party commands / plugins
6
+ #
7
+ def self.load_plugins
8
+ program = File.basename $PROGRAM_NAME
9
+ command = ARGV.first
10
+ command = ARGV[1] if command.eql? 'help' and ARGV.length > 1 # Help command
11
+
12
+ if program.eql? 'kood' # File is being imported from the bin and not from the test suite
13
+ unless command.nil? or Kood::CLI.method_defined? command # Check if command is unknown
14
+ begin
15
+ plugin_name = command # The command is the name of the plugin
16
+
17
+ # Require the plugin, which must be accessible and follow the naming convention
18
+ require "kood-plugin-#{ plugin_name }"
19
+
20
+ # Transform plugin name to a valid class name (for example, foo_bar becomes FooBar)
21
+ plugin_class_name = Thor::Util.camel_case(plugin_name)
22
+
23
+ # Get the class and register it (the plugin must extend Thor)
24
+ plugin_class = Kood::Plugin.const_get(plugin_class_name)
25
+ Kood::CLI.register(plugin_class, plugin_name, plugin_name, "Kood plugin")
26
+ rescue LoadError
27
+ # TODO Thor supports partial subcommands and aliases for subcommands. The
28
+ # `method_defined?` condition is not enough. For now, we don't exit here and
29
+ # everything should still work as expected, but this could be improved.
30
+ #
31
+ # puts "Could not find command or plugin \"#{ plugin_name }\"."
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ end