sym 0.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +25 -0
- data/.document +2 -0
- data/.gitignore +6 -2
- data/.rspec +1 -1
- data/.rubocop.yml +1156 -0
- data/.travis.yml +10 -2
- data/.yardopts +5 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/MANAGING-KEYS.md +67 -0
- data/README.md +444 -12
- data/Rakefile +10 -2
- data/bin/sym.bash-completion +24 -0
- data/exe/keychain +38 -0
- data/exe/sym +20 -0
- data/lib/sym.rb +110 -2
- data/lib/sym/app.rb +56 -0
- data/lib/sym/app/args.rb +42 -0
- data/lib/sym/app/cli.rb +192 -0
- data/lib/sym/app/commands.rb +56 -0
- data/lib/sym/app/commands/command.rb +77 -0
- data/lib/sym/app/commands/delete_keychain_item.rb +17 -0
- data/lib/sym/app/commands/encrypt_decrypt.rb +26 -0
- data/lib/sym/app/commands/generate_key.rb +37 -0
- data/lib/sym/app/commands/open_editor.rb +97 -0
- data/lib/sym/app/commands/print_key.rb +15 -0
- data/lib/sym/app/commands/show_examples.rb +76 -0
- data/lib/sym/app/commands/show_help.rb +16 -0
- data/lib/sym/app/commands/show_language_examples.rb +81 -0
- data/lib/sym/app/commands/show_version.rb +14 -0
- data/lib/sym/app/input/handler.rb +41 -0
- data/lib/sym/app/keychain.rb +135 -0
- data/lib/sym/app/nlp.rb +18 -0
- data/lib/sym/app/nlp/constants.rb +32 -0
- data/lib/sym/app/nlp/translator.rb +61 -0
- data/lib/sym/app/nlp/usage.rb +72 -0
- data/lib/sym/app/output.rb +15 -0
- data/lib/sym/app/output/base.rb +61 -0
- data/lib/sym/app/output/file.rb +18 -0
- data/lib/sym/app/output/noop.rb +14 -0
- data/lib/sym/app/output/stdout.rb +13 -0
- data/lib/sym/app/password/cache.rb +63 -0
- data/lib/sym/app/private_key/base64_decoder.rb +17 -0
- data/lib/sym/app/private_key/decryptor.rb +71 -0
- data/lib/sym/app/private_key/detector.rb +42 -0
- data/lib/sym/app/private_key/handler.rb +44 -0
- data/lib/sym/app/short_name.rb +10 -0
- data/lib/sym/application.rb +114 -0
- data/lib/sym/cipher_handler.rb +46 -0
- data/lib/sym/configuration.rb +39 -0
- data/lib/sym/data.rb +23 -0
- data/lib/sym/data/decoder.rb +28 -0
- data/lib/sym/data/encoder.rb +24 -0
- data/lib/sym/data/wrapper_struct.rb +43 -0
- data/lib/sym/encrypted_file.rb +34 -0
- data/lib/sym/errors.rb +37 -0
- data/lib/sym/extensions/class_methods.rb +12 -0
- data/lib/sym/extensions/instance_methods.rb +114 -0
- data/lib/sym/version.rb +1 -1
- data/sym.gemspec +34 -15
- metadata +224 -9
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'tsort'
|
3
|
+
require 'pp'
|
4
|
+
module Sym
|
5
|
+
module App
|
6
|
+
module Commands
|
7
|
+
|
8
|
+
class DependencyResolver < Hash
|
9
|
+
include TSort
|
10
|
+
alias tsort_each_node each_key
|
11
|
+
|
12
|
+
def tsort_each_child(node, &block)
|
13
|
+
fetch(node).each(&block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
@dependency = DependencyResolver.new
|
18
|
+
@commands = Set.new
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_accessor :commands, :dependency
|
22
|
+
|
23
|
+
def register(command_class)
|
24
|
+
self.commands << command_class
|
25
|
+
self.dependency[command_class.short_name] ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def order(command_class, after)
|
29
|
+
self.dependency[command_class.short_name].unshift(after) if after
|
30
|
+
self.dependency[command_class.short_name].flatten!
|
31
|
+
end
|
32
|
+
|
33
|
+
def dependencies
|
34
|
+
@dependencies ||= self.dependency.tsort
|
35
|
+
@dependencies
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sort commands based on the #dependencies array, which itself is sorted
|
39
|
+
# based on command dependencies.
|
40
|
+
def sorted_commands
|
41
|
+
@sorted_commands ||= self.commands.to_a.sort_by{|klass| dependencies.index(klass.short_name) }
|
42
|
+
@sorted_commands
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_command_class(opts)
|
46
|
+
self.sorted_commands.each do |command_class|
|
47
|
+
return command_class if command_class.options_satisfied_by?(opts)
|
48
|
+
end
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
require_dir 'sym/app/commands'
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'sym'
|
2
|
+
require 'sym/app'
|
3
|
+
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
module Sym
|
7
|
+
module App
|
8
|
+
module Commands
|
9
|
+
class Command
|
10
|
+
|
11
|
+
def self.inherited(klass)
|
12
|
+
klass.instance_eval do
|
13
|
+
class << self
|
14
|
+
attr_accessor :required, :incompatible
|
15
|
+
|
16
|
+
include Sym::App::ShortName
|
17
|
+
|
18
|
+
def try_after(*dependencies)
|
19
|
+
Sym::App::Commands.order(self, dependencies)
|
20
|
+
end
|
21
|
+
|
22
|
+
def required_options(*args)
|
23
|
+
self.required ||= Set.new
|
24
|
+
required.merge(args) if args
|
25
|
+
required
|
26
|
+
end
|
27
|
+
|
28
|
+
def incompatible_options(*args)
|
29
|
+
self.incompatible ||= Set.new
|
30
|
+
incompatible.merge(args) if args
|
31
|
+
incompatible
|
32
|
+
end
|
33
|
+
|
34
|
+
def options_satisfied_by?(opts_hash)
|
35
|
+
proc = required_options.find { |option| option.is_a?(Proc) }
|
36
|
+
return true if proc && proc.call(opts_hash)
|
37
|
+
return false if incompatible_options.any? { |option| opts_hash[option] }
|
38
|
+
required_options.to_a.delete_if { |o| o.is_a?(Proc) }.all? { |o|
|
39
|
+
o.is_a?(Array) ? o.any? { |opt| opts_hash[opt] } : opts_hash[o]
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Register this command with the global list.
|
45
|
+
Sym::App::Commands.register klass
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_accessor :application
|
50
|
+
|
51
|
+
def initialize(application)
|
52
|
+
self.application = application
|
53
|
+
end
|
54
|
+
|
55
|
+
def opts
|
56
|
+
application.opts
|
57
|
+
end
|
58
|
+
def opts_hash
|
59
|
+
application.opts_hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def key
|
63
|
+
@key ||= application.key
|
64
|
+
end
|
65
|
+
|
66
|
+
def execute
|
67
|
+
raise Sym::Errors::AbstractMethodCalled.new(:run)
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
"#{self.class.short_name.to_s.bold.yellow}, with options: #{application.args.argv.join(' ').gsub(/--/, '').bold.green}"
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'sym/app/keychain'
|
3
|
+
module Sym
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class DeleteKeychainItem < Command
|
7
|
+
|
8
|
+
required_options :keychain_del
|
9
|
+
try_after :generate_key, :open_editor, :encrypt_decrypt
|
10
|
+
|
11
|
+
def execute
|
12
|
+
Sym::App::KeyChain.new(opts[:keychain_del]).delete
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
module Sym
|
3
|
+
module App
|
4
|
+
module Commands
|
5
|
+
class EncryptDecrypt < Command
|
6
|
+
include Sym
|
7
|
+
|
8
|
+
required_options [ :private_key, :keyfile, :keychain, :interactive ],
|
9
|
+
[ :encrypt, :decrypt ],
|
10
|
+
[ :file, :string ]
|
11
|
+
|
12
|
+
try_after :generate_key
|
13
|
+
|
14
|
+
def execute
|
15
|
+
send(application.action, content, application.key)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def content
|
21
|
+
@content ||= (opts[:string] || (opts[:file].eql?('-') ? STDIN.read : File.read(opts[:file])))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'sym/app/keychain'
|
3
|
+
module Sym
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class GenerateKey < Command
|
7
|
+
include Sym
|
8
|
+
|
9
|
+
required_options :generate
|
10
|
+
|
11
|
+
def execute
|
12
|
+
retries ||= 0
|
13
|
+
new_private_key = self.class.create_private_key
|
14
|
+
new_private_key = encr_password(new_private_key,
|
15
|
+
application.input_handler.new_password) if opts[:password]
|
16
|
+
|
17
|
+
clipboard_copy(new_private_key) if opts[:copy]
|
18
|
+
|
19
|
+
Sym::App::KeyChain.new(opts[:keychain], opts).
|
20
|
+
add(new_private_key) if opts[:keychain] && Sym::App.is_osx?
|
21
|
+
|
22
|
+
new_private_key
|
23
|
+
rescue Sym::Errors::PasswordsDontMatch, Sym::Errors::PasswordTooShort => e
|
24
|
+
STDERR.puts e.message.bold
|
25
|
+
retry if (retries += 1) < 3
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def clipboard_copy(key)
|
31
|
+
require 'clipboard'
|
32
|
+
Clipboard.copy(key)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'sym'
|
5
|
+
require 'sym/errors'
|
6
|
+
require_relative 'command'
|
7
|
+
module Sym
|
8
|
+
module App
|
9
|
+
module Commands
|
10
|
+
class OpenEditor < Command
|
11
|
+
include Sym
|
12
|
+
|
13
|
+
required_options [ :private_key, :keyfile, :keychain, :interactive ],
|
14
|
+
:edit,
|
15
|
+
:file
|
16
|
+
|
17
|
+
try_after :generate_key, :encrypt_decrypt
|
18
|
+
|
19
|
+
attr_accessor :tempfile
|
20
|
+
|
21
|
+
def execute
|
22
|
+
begin
|
23
|
+
self.tempfile = ::Tempfile.new(::Base64.urlsafe_encode64(opts[:file]))
|
24
|
+
decrypt_content(self.tempfile)
|
25
|
+
|
26
|
+
result = process launch_editor
|
27
|
+
ensure
|
28
|
+
self.tempfile.close if tempfile
|
29
|
+
self.tempfile.unlink rescue nil
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
def launch_editor
|
35
|
+
system("#{application.editor} #{tempfile.path}")
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def decrypt_content(file)
|
41
|
+
file.open
|
42
|
+
file.write(content)
|
43
|
+
file.flush
|
44
|
+
end
|
45
|
+
|
46
|
+
def content
|
47
|
+
@content ||= decr(File.read(opts[:file]), key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def timestamp
|
51
|
+
@timestamp ||= Time.now.to_a.select { |d| d.is_a?(Fixnum) }.map { |d| '%02d' % d }[0..-3].reverse.join
|
52
|
+
end
|
53
|
+
|
54
|
+
def process(code)
|
55
|
+
if code == true
|
56
|
+
content_edited = File.read(tempfile.path)
|
57
|
+
md5 = ::Base64.encode64(Digest::MD5.new.digest(content))
|
58
|
+
md5_edited = ::Base64.encode64(Digest::MD5.new.digest(content_edited))
|
59
|
+
return 'No changes have been made.' if md5 == md5_edited
|
60
|
+
|
61
|
+
FileUtils.cp opts[:file], "#{opts[:file]}.#{timestamp}" if opts[:backup]
|
62
|
+
|
63
|
+
diff = compute_diff
|
64
|
+
|
65
|
+
File.open(opts[:file], 'w') { |f| f.write(encr(content_edited, key)) }
|
66
|
+
|
67
|
+
out = ''
|
68
|
+
if opts[:verbose]
|
69
|
+
out << "Saved encrypted/compressed content to #{opts[:file].bold.blue}" +
|
70
|
+
" (#{File.size(opts[:file]) / 1024}Kb), unencrypted size #{content.length / 1024}Kb."
|
71
|
+
out << (opts[:backup] ? ",\nbacked up the last version to #{backup_file.bold.blue}." : '.')
|
72
|
+
end
|
73
|
+
out << "\n\nDiff:\n#{diff}"
|
74
|
+
out
|
75
|
+
else
|
76
|
+
raise Sym::Errors::EditorExitedAbnormally.new("#{application.editor} exited with #{$<}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Computes the diff between two unencrypted versions
|
81
|
+
def compute_diff
|
82
|
+
original_content_file = Tempfile.new(rand(1024).to_s)
|
83
|
+
original_content_file.open
|
84
|
+
original_content_file.write(content)
|
85
|
+
original_content_file.flush
|
86
|
+
diff = `diff #{original_content_file.path} #{tempfile.path}`
|
87
|
+
diff.gsub!(/> (.*\n)/m, '\1'.green)
|
88
|
+
diff.gsub!(/< (.*\n)/m, '\1'.red)
|
89
|
+
ensure
|
90
|
+
original_content_file.close
|
91
|
+
original_content_file.unlink
|
92
|
+
diff
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'colored2'
|
2
|
+
require_relative 'command'
|
3
|
+
module Sym
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class ShowExamples < Command
|
7
|
+
required_options :examples
|
8
|
+
try_after :show_help
|
9
|
+
|
10
|
+
def execute
|
11
|
+
output = []
|
12
|
+
|
13
|
+
output << example(comment: 'generate a new private key into an environment variable:',
|
14
|
+
command: 'export KEY=$(sym -g)',
|
15
|
+
echo: 'echo $KEY',
|
16
|
+
result: '75ngenJpB6zL47/8Wo7Ne6JN1pnOsqNEcIqblItpfg4='.green)
|
17
|
+
|
18
|
+
output << example(comment: 'generate a new password-protected key, copy to the clipboard & save to a file',
|
19
|
+
command: 'sym -gpc -o ~/.key',
|
20
|
+
echo: 'New Password : ' + '••••••••••'.green,
|
21
|
+
result: 'Confirm Password : ' + '••••••••••'.green)
|
22
|
+
|
23
|
+
output << example(comment: 'encrypt a plain text string with a key, and save the output to a file',
|
24
|
+
command: 'sym -e -s ' + '"secret string"'.bold.yellow + ' -k $KEY -o file.enc',
|
25
|
+
echo: 'cat file.enc',
|
26
|
+
result: 'Y09MNDUyczU1S0UvelgrLzV0RTYxZz09CkBDMEw4Q0R0TmpnTm9md1QwNUNy%T013PT0K'.green)
|
27
|
+
|
28
|
+
output << example(comment: 'decrypt a previously encrypted string:',
|
29
|
+
command: 'sym -d -s $(cat file.enc) -k $KEY',
|
30
|
+
result: 'secret string'.green)
|
31
|
+
|
32
|
+
output << example(comment: 'encrypt sym.yml and save it to sym.enc:',
|
33
|
+
command: 'sym -e -f sym.yml -o sym.enc -k $KEY')
|
34
|
+
|
35
|
+
output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
|
36
|
+
command: 'sym -df sym.enc -k $KEY')
|
37
|
+
|
38
|
+
output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, create a backup',
|
39
|
+
command: 'sym -tibf ecrets.enc',
|
40
|
+
result: '
|
41
|
+
Private Key: ••••••••••••••••••••••••••••••••••••••••••••
|
42
|
+
Saved encrypted content to sym.enc.
|
43
|
+
|
44
|
+
Diff:
|
45
|
+
3c3
|
46
|
+
'.white.dark + '# (c) 2015 Konstantin Gredeskoul. All rights reserved.'.red.bold + '
|
47
|
+
---' + '
|
48
|
+
# (c) 2016 Konstantin Gredeskoul. All rights reserved.'.green.bold)
|
49
|
+
|
50
|
+
|
51
|
+
if Sym::App.is_osx?
|
52
|
+
output << example(comment: 'generate a new password-encrypted key, save it to your Keychain:',
|
53
|
+
command: 'sym -gpx mykey -o ~/.key')
|
54
|
+
|
55
|
+
output << example(comment: 'use the new key to encrypt a file:',
|
56
|
+
command: 'sym -x mykey -e -f password.txt -o passwords.enc')
|
57
|
+
|
58
|
+
output << example(comment: 'use the new key to inline-edit the encrypted file:',
|
59
|
+
command: 'sym -x mykey -t -f sym.yml')
|
60
|
+
end
|
61
|
+
|
62
|
+
output.flatten.compact.join("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
def example(comment: nil, command: nil, echo: nil, result: nil)
|
66
|
+
out = []
|
67
|
+
out << "# #{comment}".white.dark.italic if comment
|
68
|
+
out << "#{command}" if command
|
69
|
+
out << "#{echo}" if echo
|
70
|
+
out << "#{result}" if result
|
71
|
+
out << '—'*80
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
module Sym
|
3
|
+
module App
|
4
|
+
module Commands
|
5
|
+
class ShowHelp < Command
|
6
|
+
|
7
|
+
required_options :help, ->(opts) { opts.to_hash.keys.all? { |k| !opts[k] } }
|
8
|
+
try_after :generate_key, :open_editor, :encrypt_decrypt
|
9
|
+
|
10
|
+
def execute
|
11
|
+
opts.to_s(prefix: ' ' * 2)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'colored2'
|
2
|
+
require_relative 'command'
|
3
|
+
require_relative '../nlp'
|
4
|
+
module Sym
|
5
|
+
module App
|
6
|
+
module Commands
|
7
|
+
class ShowLanguageExamples < Command
|
8
|
+
required_options :language
|
9
|
+
try_after :show_help
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def execute
|
14
|
+
output = []
|
15
|
+
|
16
|
+
output << Sym::App::NLP::Base.usage
|
17
|
+
|
18
|
+
output << example(comment: 'generate a new private key and copy to the clipboard but do not print to terminal',
|
19
|
+
command: 'sym create new key to clipboard quietly'
|
20
|
+
)
|
21
|
+
|
22
|
+
output << example(comment: 'generate and save to a file a password-protected key, silently',
|
23
|
+
command: 'sym create a secure key and save it to "my.key"',
|
24
|
+
)
|
25
|
+
|
26
|
+
output << example(comment: 'encrypt a plain text string with a key, and save the output to a file',
|
27
|
+
command: 'sym encrypt string "secret string" using $(cat my.key) save to file.enc')
|
28
|
+
|
29
|
+
output << example(comment: 'decrypt a previously encrypted string:',
|
30
|
+
command: 'sym decrypt string $ENC using $(cat my.key)')
|
31
|
+
|
32
|
+
output << example(comment: 'encrypt "file.txt" with key from my.key and save it to file.enc',
|
33
|
+
command: 'sym encrypt file file.txt with key from my.key and save it to file.enc')
|
34
|
+
|
35
|
+
output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
|
36
|
+
command: 'sym decrypt file file.enc with key from "my.key"')
|
37
|
+
|
38
|
+
output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, and create a backup upon save',
|
39
|
+
command: 'sym edit file file.enc ask for a key and make a backup',
|
40
|
+
)
|
41
|
+
|
42
|
+
if Sym::App.is_osx?
|
43
|
+
output << example(comment: 'generate a new password-encrypted key, save it to your Keychain:',
|
44
|
+
command: 'sym create a new protected key store in keychain "my-keychain-key"')
|
45
|
+
|
46
|
+
output << example(comment: 'print the key stored in the keychain item "my-keychain-key"',
|
47
|
+
command: 'sym print keychain "my-keychain-key"')
|
48
|
+
|
49
|
+
output << example(comment: 'use the new key to encrypt a file:',
|
50
|
+
command: 'sym encrypt with keychain "my-keychain-key" file "password.txt" and write to "passwords.enc"')
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
output.flatten.compact.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
def example(comment: nil, command: nil, echo: nil, result: nil)
|
58
|
+
@dict ||= ::Sym::App::NLP::Constants::DICTIONARY.to_a.flatten!
|
59
|
+
_command = command.split(' ').map do |w|
|
60
|
+
_w = w.to_sym
|
61
|
+
if w == 'sym'
|
62
|
+
w.italic.yellow
|
63
|
+
elsif ::Sym::App::NLP::Constants::STRIPPED.include?(_w)
|
64
|
+
w.italic.red
|
65
|
+
elsif @dict.include?(_w)
|
66
|
+
w.blue
|
67
|
+
else
|
68
|
+
w
|
69
|
+
end
|
70
|
+
end.join(' ') if command
|
71
|
+
out = []
|
72
|
+
out << "# #{comment}".white.dark.italic if comment
|
73
|
+
out << "#{_command}" if command
|
74
|
+
out << "#{echo}" if echo
|
75
|
+
out << "#{result}" if result
|
76
|
+
out << (' '*80).dark
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|