tty_slides 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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +44 -0
- data/Rakefile +1 -0
- data/lib/cml/cml.rb +123 -0
- data/lib/helpers/curses.rb +55 -0
- data/lib/helpers/helper.rb +4 -0
- data/lib/main_window.rb +19 -0
- data/lib/options.rb +16 -0
- data/lib/q_and_a_client.rb +36 -0
- data/lib/q_and_a_window.rb +15 -0
- data/lib/slide_list.rb +19 -0
- data/lib/tty_slides/version.rb +3 -0
- data/lib/tty_slides.rb +51 -0
- data/tty_slides.gemspec +22 -0
- metadata +93 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Mark Lorenz
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# TTY Slides
|
2
|
+
|
3
|
+
> Gem for presenting slides with the terminal
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
gem install tty_slides
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
```ruby
|
13
|
+
# define the BASE_PATH and SLIDES
|
14
|
+
class SlideList
|
15
|
+
BASE_PATH = Pathname.new(ROOT) + ".." + "slides"
|
16
|
+
|
17
|
+
SLIDES = [
|
18
|
+
"introduction",
|
19
|
+
"covermymeds",
|
20
|
+
]
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
``bash
|
25
|
+
|
26
|
+
```
|
27
|
+
|
28
|
+
## Adding Slides
|
29
|
+
Slides are stored in bastardized version of HTML that can be rendred with n-curses. They are found in the `doc/` folder
|
30
|
+
### CML specification
|
31
|
+
- `<head>`: the text node in this element is positioned at the top left of the screen.
|
32
|
+
- `<body>`: contains any number of `<div>`, `<span>`, `<code>` or `<line>` tags.
|
33
|
+
- `<div>` and `<span>`: go togeather to make a vertially and horizontally centered line. Each div can be no longer than one line.
|
34
|
+
- `<code>` and `<line>`: go togeather to make left aligned text that is in a horizontally and vertically centered box. Each line, should be one line. Duh.
|
35
|
+
- attributes must be curses attribute calls e.g.
|
36
|
+
`Curses.color_pair(RED_COLOR)` becomes `color_pair(RED_COLOR)`
|
37
|
+
|
38
|
+
## Contributing
|
39
|
+
|
40
|
+
1. Fork it
|
41
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
42
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
43
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
44
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/cml/cml.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
class CML
|
3
|
+
PADDING = 2
|
4
|
+
|
5
|
+
attr_accessor :window
|
6
|
+
attr_accessor :text
|
7
|
+
|
8
|
+
def initialize window, text
|
9
|
+
@window = window
|
10
|
+
@text = text
|
11
|
+
@document = parse
|
12
|
+
end
|
13
|
+
|
14
|
+
def render
|
15
|
+
@window.clear
|
16
|
+
render_loop @document
|
17
|
+
@window.refresh
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def vertically_center(fragment)
|
23
|
+
top_line = (@window.maxy - fragment.css('div,line').size) / 2 - PADDING
|
24
|
+
@window.setpos(top_line, @window.curx)
|
25
|
+
end
|
26
|
+
|
27
|
+
def horizontally_center(text)
|
28
|
+
left_pos = (@window.maxx - text.size) / 2
|
29
|
+
@window.setpos(@window.cury+1, left_pos)
|
30
|
+
end
|
31
|
+
|
32
|
+
def left_align(fragment)
|
33
|
+
left_pos = PADDING
|
34
|
+
@current_offset ||= 0
|
35
|
+
@window.setpos(@window.cury + 1, left_pos + @current_offset)
|
36
|
+
end
|
37
|
+
|
38
|
+
def offset_for_code_box(fragment)
|
39
|
+
largest_fragment = fragment.css('line').map(&:text).max_by(&:length)
|
40
|
+
@current_offset = (@window.maxx - largest_fragment.size) / 2
|
41
|
+
end
|
42
|
+
|
43
|
+
# DFS
|
44
|
+
def render_loop(fragment)
|
45
|
+
recurse = ->(){
|
46
|
+
fragment.children.each do |child|
|
47
|
+
render_loop(child)
|
48
|
+
end
|
49
|
+
}
|
50
|
+
|
51
|
+
if fragment.node_name == "div"
|
52
|
+
div fragment[:style], fragment.text, &recurse
|
53
|
+
elsif fragment.node_name == "title"
|
54
|
+
title fragment[:style], fragment.text, &recurse
|
55
|
+
elsif fragment.node_name == "body"
|
56
|
+
body fragment[:style], fragment, &recurse
|
57
|
+
elsif fragment.node_name == "code"
|
58
|
+
code fragment[:style], fragment, &recurse
|
59
|
+
elsif fragment.node_name == "line"
|
60
|
+
line fragment[:style], fragment.text, &recurse
|
61
|
+
elsif fragment.node_name == "span"
|
62
|
+
span fragment[:style], &recurse
|
63
|
+
elsif fragment.node_name == "text"
|
64
|
+
text fragment.text, &recurse
|
65
|
+
else
|
66
|
+
recurse.call
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse
|
71
|
+
Nokogiri::HTML.parse(@text)
|
72
|
+
end
|
73
|
+
|
74
|
+
def title(attrs, text, &block)
|
75
|
+
@window.to_title_position
|
76
|
+
attrib *split_attrs(attrs), &block
|
77
|
+
end
|
78
|
+
|
79
|
+
def body(attrs, fragment, &block)
|
80
|
+
vertically_center(fragment)
|
81
|
+
attrib *split_attrs(attrs), &block
|
82
|
+
end
|
83
|
+
|
84
|
+
def div(attrs, text, &block)
|
85
|
+
@window.setpos(@window.cury + 1, @window.curx)
|
86
|
+
horizontally_center(text)
|
87
|
+
attrib *split_attrs(attrs), &block
|
88
|
+
end
|
89
|
+
|
90
|
+
def span(attrs, &block)
|
91
|
+
attrib *split_attrs(attrs), &block
|
92
|
+
end
|
93
|
+
|
94
|
+
def line(attrs, fragment, &block)
|
95
|
+
left_align(fragment)
|
96
|
+
attrib *split_attrs(attrs), &block
|
97
|
+
end
|
98
|
+
|
99
|
+
def code(attrs, fragment, &block)
|
100
|
+
offset_for_code_box(fragment)
|
101
|
+
attrib *split_attrs(attrs), &block
|
102
|
+
end
|
103
|
+
|
104
|
+
def split_attrs(attrs_string)
|
105
|
+
attrs_string.to_s.split(",")
|
106
|
+
end
|
107
|
+
|
108
|
+
def text(text)
|
109
|
+
@window << text unless text.match(/^\s+$/) # squash white space like real HTML
|
110
|
+
yield
|
111
|
+
end
|
112
|
+
|
113
|
+
def attrib *attributes
|
114
|
+
attributes.each do |attribute|
|
115
|
+
@window.attron Curses.instance_eval(attribute)
|
116
|
+
end
|
117
|
+
yield
|
118
|
+
attributes.each do |attribute|
|
119
|
+
@window.attroff Curses.instance_eval(attribute)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'curses'
|
2
|
+
module Helper::Curses
|
3
|
+
|
4
|
+
Curses.start_color
|
5
|
+
ORANGE_COLOR = 20
|
6
|
+
RUBINE_COLOR = 10
|
7
|
+
LIGHT_GRAY_COLOR = 91
|
8
|
+
DARK_GRAY_COLOR = 92
|
9
|
+
Curses.init_color ORANGE_COLOR, 1000, 400, 0
|
10
|
+
Curses.init_color RUBINE_COLOR, 800, 0, 250
|
11
|
+
Curses.init_color LIGHT_GRAY_COLOR, 500, 500, 500
|
12
|
+
Curses.init_color DARK_GRAY_COLOR, 250, 250, 250
|
13
|
+
|
14
|
+
BLUE = 30
|
15
|
+
RED = 70
|
16
|
+
CYAN = 40
|
17
|
+
GREEN = 50
|
18
|
+
MAGENTA = 60
|
19
|
+
YELLOW = 80
|
20
|
+
WHITE = 90
|
21
|
+
ORANGE = 20
|
22
|
+
RUBINE = 10
|
23
|
+
LIGHT_GRAY = 91
|
24
|
+
DARK_GRAY = 92
|
25
|
+
|
26
|
+
Curses.init_pair BLUE, Curses::COLOR_BLUE, Curses::COLOR_BLACK
|
27
|
+
Curses.init_pair RED, Curses::COLOR_RED, Curses::COLOR_BLACK
|
28
|
+
Curses.init_pair CYAN, Curses::COLOR_CYAN, Curses::COLOR_BLACK
|
29
|
+
Curses.init_pair GREEN, Curses::COLOR_GREEN, Curses::COLOR_BLACK
|
30
|
+
Curses.init_pair MAGENTA, Curses::COLOR_MAGENTA, Curses::COLOR_BLACK
|
31
|
+
Curses.init_pair YELLOW, Curses::COLOR_YELLOW, Curses::COLOR_BLACK
|
32
|
+
Curses.init_pair WHITE, Curses::COLOR_WHITE, Curses::COLOR_BLACK
|
33
|
+
Curses.init_pair ORANGE, ORANGE_COLOR, Curses::COLOR_BLACK
|
34
|
+
Curses.init_pair RUBINE, RUBINE_COLOR, Curses::COLOR_BLACK
|
35
|
+
Curses.init_pair LIGHT_GRAY, LIGHT_GRAY_COLOR, Curses::COLOR_BLACK
|
36
|
+
Curses.init_pair DARK_GRAY, DARK_GRAY_COLOR, Curses::COLOR_BLACK
|
37
|
+
|
38
|
+
def attrib *attributes
|
39
|
+
attributes.each do |attribute|
|
40
|
+
Curses.attron attribute
|
41
|
+
end
|
42
|
+
yield
|
43
|
+
attributes.each do |attribute|
|
44
|
+
Curses.attroff attribute
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def v_center_text text
|
49
|
+
Curses.lines / 2
|
50
|
+
end
|
51
|
+
|
52
|
+
def h_center_text text
|
53
|
+
(Curses.cols / 2) - (text.length / 2)
|
54
|
+
end
|
55
|
+
end
|
data/lib/main_window.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
class MainWindow < SimpleDelegator
|
4
|
+
def initialize window
|
5
|
+
super
|
6
|
+
refresh
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_title_position
|
10
|
+
setpos(1, 1)
|
11
|
+
end
|
12
|
+
|
13
|
+
def refresh
|
14
|
+
box("|", "-")
|
15
|
+
setpos(0, 0)
|
16
|
+
addstr('[ Ruby Fail ]')
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
data/lib/options.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ostruct'
|
3
|
+
module Options
|
4
|
+
def self.parse!
|
5
|
+
options = OpenStruct.new host: "localhost", port: 3000,
|
6
|
+
start_at: 0
|
7
|
+
|
8
|
+
OptionParser.new do |opts|
|
9
|
+
opts.on("-h", "--host_name=val", String) { |arg| options.host = arg }
|
10
|
+
opts.on("-p", "--port=val", Integer) { |arg| options.port = arg }
|
11
|
+
opts.on("-s", "--start_at=", Integer) { |arg| options.start_at = arg }
|
12
|
+
opts.on("", "--help") { exec "more #{__FILE__}" }
|
13
|
+
end.parse!
|
14
|
+
options
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
class QandAClient
|
4
|
+
def initialize q_and_a_window, host="localhost", port=3000
|
5
|
+
@q_and_a_window = q_and_a_window
|
6
|
+
@host = host || "localhost"
|
7
|
+
@port = port.zero? ? 3000 : port
|
8
|
+
@socket = connect
|
9
|
+
@q_and_a_window.scrollok true
|
10
|
+
listen if connected?
|
11
|
+
end
|
12
|
+
|
13
|
+
def connect
|
14
|
+
TCPSocket.new @host, @port
|
15
|
+
rescue
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def connected?
|
20
|
+
!@socket.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def listen
|
26
|
+
Thread.new {
|
27
|
+
loop do
|
28
|
+
@q_and_a_window.setpos 0, 1
|
29
|
+
@q_and_a_window << "> "
|
30
|
+
@q_and_a_window << @socket.readline
|
31
|
+
@q_and_a_window.scrl -1
|
32
|
+
@q_and_a_window.refresh
|
33
|
+
end
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
data/lib/slide_list.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class SlideList
|
2
|
+
attr_reader :current
|
3
|
+
|
4
|
+
def initialize(start = 0)
|
5
|
+
@current = start - 1
|
6
|
+
end
|
7
|
+
|
8
|
+
def next
|
9
|
+
@current += 1
|
10
|
+
@current = SLIDES.size - 1 if @current >= SLIDES.size
|
11
|
+
BASE_PATH + "#{SLIDES[@current]}.cml"
|
12
|
+
end
|
13
|
+
|
14
|
+
def previous
|
15
|
+
@current -= 1
|
16
|
+
@current = 0 if @current < 0
|
17
|
+
BASE_PATH + "#{SLIDES[@current]}.cml"
|
18
|
+
end
|
19
|
+
end
|
data/lib/tty_slides.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# lazy options parsing:
|
2
|
+
# ARGV[0] - the slide you want to start on
|
3
|
+
# ARGV[1] - the q and q server hostname
|
4
|
+
# ARGV[2] - the q and q server port
|
5
|
+
require 'curses'
|
6
|
+
require 'io/console'
|
7
|
+
|
8
|
+
ROOT = File.dirname(__FILE__)
|
9
|
+
|
10
|
+
require 'tty_slides/version'
|
11
|
+
require 'helpers/helper'
|
12
|
+
require 'cml/cml'
|
13
|
+
require 'main_window'
|
14
|
+
require 'q_and_a_window'
|
15
|
+
require 'q_and_a_client'
|
16
|
+
require 'slide_list'
|
17
|
+
require 'options'
|
18
|
+
|
19
|
+
options = Options.parse!
|
20
|
+
|
21
|
+
Curses.init_screen
|
22
|
+
Curses.noecho
|
23
|
+
Curses.curs_set(0) # no cursor please
|
24
|
+
|
25
|
+
height = Curses.lines
|
26
|
+
width = Curses.cols
|
27
|
+
main_window = MainWindow.new(Curses::Window.new height - 5, width, 0, 0)
|
28
|
+
q_and_a = QandAWindow.new(Curses::Window.new 5, width, main_window.maxy, 0)
|
29
|
+
slides = SlideList.new(options.start_at)
|
30
|
+
|
31
|
+
q_a_client = QandAClient.new(q_and_a, options.host, options.port)
|
32
|
+
if !q_a_client.connected?
|
33
|
+
main_window.resize height, width
|
34
|
+
Curses.refresh
|
35
|
+
end
|
36
|
+
|
37
|
+
CML.new(main_window, File.read(slides.next)).render
|
38
|
+
|
39
|
+
at_exit { puts "Last Slide: #{slides.current}" if $!.kind_of?(StandardError) }
|
40
|
+
at_exit { Curses.close_screen }
|
41
|
+
|
42
|
+
loop do
|
43
|
+
case STDIN.getch
|
44
|
+
when /b/i
|
45
|
+
CML.new(main_window, File.read(slides.previous)).render
|
46
|
+
when /q/i
|
47
|
+
exit
|
48
|
+
else
|
49
|
+
CML.new(main_window, File.read(slides.next)).render
|
50
|
+
end
|
51
|
+
end
|
data/tty_slides.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tty_slides/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "tty_slides"
|
8
|
+
gem.version = TtySlides::VERSION
|
9
|
+
gem.authors = ["Mark Lorenz"]
|
10
|
+
gem.email = ["markjlorenz@dapplebeforedawn.com"]
|
11
|
+
gem.description = %q{Do a presentation from the terminal.}
|
12
|
+
gem.summary = %q{You provides the slides, we'll do the rest.}
|
13
|
+
gem.homepage = "https://github.com/dapplebeforedawn/tty_slides"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
|
19
|
+
gem.add_development_dependency "pry"
|
20
|
+
|
21
|
+
gem.add_dependency "nokogiri"
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tty_slides
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mark Lorenz
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-02-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: pry
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: nokogiri
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Do a presentation from the terminal.
|
47
|
+
email:
|
48
|
+
- markjlorenz@dapplebeforedawn.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- .gitignore
|
54
|
+
- Gemfile
|
55
|
+
- LICENSE.txt
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- lib/cml/cml.rb
|
59
|
+
- lib/helpers/curses.rb
|
60
|
+
- lib/helpers/helper.rb
|
61
|
+
- lib/main_window.rb
|
62
|
+
- lib/options.rb
|
63
|
+
- lib/q_and_a_client.rb
|
64
|
+
- lib/q_and_a_window.rb
|
65
|
+
- lib/slide_list.rb
|
66
|
+
- lib/tty_slides.rb
|
67
|
+
- lib/tty_slides/version.rb
|
68
|
+
- tty_slides.gemspec
|
69
|
+
homepage: https://github.com/dapplebeforedawn/tty_slides
|
70
|
+
licenses: []
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.24
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: You provides the slides, we'll do the rest.
|
93
|
+
test_files: []
|