tabscroll 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a07da9616f2254f2fd9bd14e11a5e8f327c132df
4
+ data.tar.gz: 854189ab82e34c155233f7b5b9b8ac0768105761
5
+ SHA512:
6
+ metadata.gz: 00977e49cde6a2ccf4666510585224a82fdd4efbc0d38b9e3f38008eced4f969d0d63b0b693285bea776feb6aa2589be7decd1ebba1346365c1ede6e80df4e86
7
+ data.tar.gz: d1a0f44480d736aa8f1c9a7b2f171d873d79b3d66c9a4d715b85eabee982552606c9d1aaa23cba8e37795b0136a7a11a6086f15b7be804154d5e68a437187ef6
data/bin/tabscroll ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # The main `tabscroll` executable
4
+
5
+ require 'tabscroll'
6
+
7
+ begin
8
+ filename = ARGV[0]
9
+ if not filename
10
+ puts "Usage:"
11
+ puts " $ #{PROGRAM_NAME} (filename)"
12
+ exit 666
13
+ end
14
+
15
+ $engine = Engine.new
16
+ if not $engine
17
+ puts 'Failed to start curses!'
18
+ exit 1337
19
+ end
20
+ $engine.timeout 10
21
+
22
+ win = Screen.new(0, 1, $engine.width, $engine.height - 2)
23
+ track = Track.new win
24
+ track.load filename
25
+ track.auto_scroll true
26
+
27
+ titlebar = Screen.new(0, 0, $engine.width, 1)
28
+ statusbar = Screen.new(0, ($engine.height - 1), $engine.width, 1)
29
+
30
+ bars_hidden = false
31
+ finished = false
32
+ while not finished
33
+ # WHY DOES GETCH CLEARS UP THE WHOLE SCREEN?
34
+ # IT DOESNT MAKE ANY SENSE
35
+ c = $engine.getchar
36
+ case c
37
+ when 'e'
38
+ track.end
39
+ when 'a'
40
+ track.begin
41
+ when 'h'
42
+ show_help_window
43
+ when 'o'
44
+ bars_hidden = (bars_hidden ? false : true)
45
+ Curses::clear
46
+ when '<'
47
+ track.scroll -5
48
+ when '>'
49
+ track.scroll 5
50
+ when Curses::KEY_LEFT
51
+ track.speed -= 1
52
+ when Curses::KEY_RIGHT
53
+ track.speed += 1
54
+ when Curses::KEY_DOWN, Curses::KEY_UP
55
+ track.speed = 0
56
+ when 'q'
57
+ quit
58
+ end
59
+
60
+ track.update
61
+ track.show
62
+ if not bars_hidden
63
+ titlebar.mvaddstr(0, 0, "#{filename} (#{track.percent_completed}%) ", Engine::Colors[:cyan])
64
+ titlebar.mvaddstr_right(0, " Speed: #{track.speed}", Engine::Colors[:cyan])
65
+
66
+ statusbar.mvaddstr_left(0, "#{PROGRAM_NAME} v#{PROGRAM_VERSION} - press `h` for help", Engine::Colors[:green])
67
+ end
68
+ end
69
+
70
+ quit
71
+ end
72
+
@@ -0,0 +1,95 @@
1
+
2
+ require 'curses'
3
+
4
+ # The main interface with Curses.
5
+ #
6
+ # This acts as a middleman, abstracting away curses' details.
7
+ class Engine
8
+
9
+ # All possible colors.
10
+ Colors = {
11
+ :black => 1,
12
+ :white => 2,
13
+ :red => 3,
14
+ :yellow => 4,
15
+ :magenta => 5,
16
+ :blue => 6,
17
+ :green => 7,
18
+ :cyan => 8
19
+ }.freeze
20
+
21
+ # Initializes Ncurses with minimal +width+ and +height+.
22
+ #
23
+ def initialize(min_width=nil, min_height=nil)
24
+ @has_colors = nil
25
+
26
+ @screen = Curses::init_screen
27
+ return nil if not @screen
28
+
29
+ if min_width and min_height
30
+ cur_width = @screen.maxx
31
+ cur_height = @screen.maxy
32
+
33
+ if cur_width < @width or cur_height < @height
34
+ self.exit
35
+ $stderr << "Error: Screen size too small (#{cur_width}x#{cur_height})\n"
36
+ $stderr << "Please resize your terminal to at least #{@width}x#{@height}\n"
37
+ return nil
38
+ end
39
+ end
40
+
41
+ @has_colors = Curses.has_colors?
42
+ if @has_colors
43
+ Curses.start_color
44
+ Curses.use_default_colors # will use default background
45
+
46
+ # Initializes: constant foreground bg
47
+ Curses.init_pair(Colors[:white], Curses::COLOR_BLACK, -1)
48
+ Curses.init_pair(Colors[:blue], Curses::COLOR_BLUE, -1)
49
+ Curses.init_pair(Colors[:red], Curses::COLOR_RED, -1)
50
+ Curses.init_pair(Colors[:green], Curses::COLOR_GREEN, -1)
51
+ Curses.init_pair(Colors[:magenta], Curses::COLOR_MAGENTA, -1)
52
+ Curses.init_pair(Colors[:yellow], Curses::COLOR_YELLOW, -1)
53
+ Curses.init_pair(Colors[:cyan], Curses::COLOR_CYAN, -1)
54
+ end
55
+
56
+ Curses::cbreak
57
+ Curses::curs_set 0
58
+ Curses::noecho
59
+ Curses::nonl
60
+ Curses::stdscr.keypad = true # extra keys
61
+ end
62
+
63
+ def width
64
+ return Curses::cols
65
+ end
66
+
67
+ def height
68
+ return Curses::lines
69
+ end
70
+
71
+ def exit
72
+ Curses::refresh
73
+ Curses::close_screen
74
+ end
75
+
76
+ def set_color color
77
+ if @has_colors
78
+ @screen.attron Curses::color_pair(color)
79
+ return self
80
+ else
81
+ return nil
82
+ end
83
+ end
84
+
85
+ def getchar
86
+ return Curses::getch
87
+ end
88
+
89
+ # +timeout+ says how many milliseconds we wait for a key to be
90
+ # pressed.
91
+ def timeout timeout
92
+ Curses::timeout = timeout
93
+ end
94
+ end
95
+
@@ -0,0 +1,60 @@
1
+
2
+ require_relative 'screen.rb'
3
+
4
+ # A simple centralized popup.
5
+ #
6
+ # It resizes as you place the text on it.
7
+ class Popup < Screen
8
+
9
+ def initialize(title, text)
10
+ @title = title
11
+ @text = []
12
+ text.each_line do |line|
13
+ @text += [line.chomp]
14
+ end
15
+
16
+ max_width = title.length
17
+ max_height = 1
18
+
19
+ @text.each do |line|
20
+ max_width = line.length if line.length > max_width
21
+ max_height += 1
22
+ end
23
+
24
+ max_width += 2 # left-right borders
25
+ max_height += 1 # down border
26
+
27
+ x = Curses::cols/2 - max_width/2
28
+ y = Curses::lines/2 - max_height/2
29
+
30
+ super(x, y, max_width, max_height)
31
+ self.background ' '
32
+ self.box
33
+
34
+ self.mvaddstr_center(0, title, Engine::Colors[:cyan])
35
+
36
+ y = 1
37
+ @text.each do |line|
38
+ self.mvaddstr(1, y, line)
39
+ y += 1
40
+ end
41
+ end
42
+
43
+ def show
44
+ finished = false
45
+ while not finished
46
+ c = Curses::getch
47
+ case c
48
+ when 'q'
49
+ return true
50
+ when 'h'
51
+ finished = true
52
+ end
53
+ end
54
+
55
+ Curses::stdscr.clear
56
+ Curses::stdscr.refresh
57
+ return false
58
+ end
59
+ end
60
+
@@ -0,0 +1,125 @@
1
+
2
+ require 'curses'
3
+
4
+ # A segment of the terminal screen.
5
+ #
6
+ # BUG WARNING HACK FUCK
7
+ # Whenever I use @win.attrset/@win.setpos/@win.addch
8
+ # it doesn't work at all.
9
+ # Apparently, when I do this, Curses::getch clears up
10
+ # the entire screen.
11
+ #
12
+ # DO NOT DO THIS
13
+ #
14
+ class Screen
15
+ attr_reader :width, :height
16
+
17
+ # Creates a Screen at `x` `y` `w` `h`.
18
+ def initialize(x, y, w, h)
19
+ @win = Curses::Window.new(h, w, y, x)
20
+ @width = w
21
+ @height = h
22
+ end
23
+
24
+ # Sets the current color of the Screen.
25
+ def set_color color
26
+ Curses::attrset(Curses::color_pair color)
27
+ end
28
+
29
+ # Executes a block of code encapsulated within a color on/off.
30
+ # Note that the color can be overrided.
31
+ def with_color(color=nil)
32
+ Curses::attron(Curses::color_pair color) if color
33
+ yield
34
+ Curses::attroff(Curses::color_pair color) if color
35
+ end
36
+
37
+ # Puts a character +c+ on (+x+, +y+) with optional +color+.
38
+ def mvaddch(x, y, c, color=nil)
39
+ return if x < 0 or x >= @width
40
+ return if y < 0 or y >= @height
41
+
42
+ self.with_color color do
43
+ Curses::setpos(@win.begy + y, @win.begx + x)
44
+ Curses::addch c
45
+ end
46
+ end
47
+
48
+ # Puts a string +str+ on (+x+, +y+) with optional +color+.
49
+ def mvaddstr(x, y, str, color=nil)
50
+ return if x < 0 or x >= @width
51
+ return if y < 0 or y >= @height
52
+
53
+ self.with_color color do
54
+ # @win.setpos(@win.begy + y, @win.begx + x)
55
+ # @win.addstr str
56
+ Curses::setpos(@win.begy + y, @win.begx + x)
57
+ Curses::addstr str
58
+ end
59
+ end
60
+
61
+ # Puts a string +str+ centered on +y+ with optional +color+.
62
+ def mvaddstr_center(y, str, color=nil)
63
+ x = (@width/2) - (str.length/2)
64
+ self.mvaddstr(x, y, str, color)
65
+ end
66
+
67
+ def mvaddstr_left(y, str, color=nil)
68
+ self.mvaddstr(0, y, str, color)
69
+ end
70
+
71
+ def mvaddstr_right(y, str, color=nil)
72
+ x = @width - str.length
73
+ self.mvaddstr(x, y, str, color)
74
+ end
75
+
76
+ # Erases all of the Screen's contents
77
+ def clear
78
+ @win.clear
79
+ end
80
+
81
+ # Commits the changes on the Screen.
82
+ def refresh
83
+ @win.refresh
84
+ end
85
+
86
+ # Moves window so that the upper-left corner is at `x` `y`.
87
+ def move(x, y)
88
+ @win.move(y, x)
89
+ end
90
+
91
+ # Resizes window to +width+ and +h+eight.
92
+ def resize(w, h)
93
+ @win.resize(h, w)
94
+ @width = w
95
+ @height = h
96
+ end
97
+
98
+ # Set block/nonblocking reads for window.
99
+ #
100
+ # * If `delay` is negative, blocking read is used.
101
+ # * If `delay` is zero, nonblocking read is used.
102
+ # * If `delay` is positive, waits for `delay` milliseconds and
103
+ # returns ERR of no input.
104
+ def timeout(delay=-1)
105
+ @win.timeout = delay
106
+ end
107
+
108
+ def background char
109
+ @win.bkgd char
110
+ @win.refresh
111
+ end
112
+
113
+ # Sets the Screen border.
114
+ #
115
+ # * If all arguments are set, that's ok.
116
+ # * If only the first 2 arguments are set, they are the vertical
117
+ # and horizontal chars.
118
+ #
119
+ def box(horizontal=0, vertical=0)
120
+ @win.box(horizontal, vertical)
121
+ @win.refresh
122
+ end
123
+
124
+ end
125
+
@@ -0,0 +1,89 @@
1
+ # A simple timer that counts in seconds.
2
+ #
3
+ # Usage:
4
+ # timer = Timer.new
5
+ # timer.start
6
+ # ...
7
+ # if timer.delta > 0.5 # half a second
8
+ # ...
9
+ #
10
+ class Timer
11
+
12
+ def initialize
13
+ @is_running = false
14
+ @is_paused = false
15
+ end
16
+
17
+ # Starts counting.
18
+ def start
19
+ return if @is_running
20
+
21
+ @start_time = Time.now
22
+ @stop_time = 0.0
23
+ @paused_time = 0.0
24
+ @is_running = true
25
+ @is_paused = false
26
+ end
27
+
28
+ # Stops counting.
29
+ def stop
30
+ return if not @is_running
31
+
32
+ @stop_time = Time.now
33
+ @is_running = false
34
+ @is_paused = false
35
+ end
36
+
37
+ def restart
38
+ self.stop
39
+ self.start
40
+ end
41
+
42
+ def pause
43
+ return if not @is_running or @is_paused
44
+
45
+ @paused_time = (Time.now - @start_time)
46
+ @is_running = false
47
+ @is_paused = true
48
+ end
49
+
50
+ def unpause
51
+ return if not @is_paused or @is_running
52
+
53
+ @start_time = (Time.now - @paused_time)
54
+ @is_running = true
55
+ @is_paused = false
56
+ end
57
+
58
+ def running?
59
+ @is_running
60
+ end
61
+
62
+ def paused?
63
+ @is_paused
64
+ end
65
+
66
+ # Returns the current delta in seconds (float).
67
+ def delta
68
+ if @is_running
69
+ return (Time.now.to_f - @start_time.to_f)
70
+ end
71
+
72
+ return @paused_time.to_f if @is_paused
73
+
74
+ return @start_time if @start_time == 0 # Something's wrong
75
+
76
+ return (@stop_time.to_f - @start_time.to_f)
77
+ end
78
+
79
+ # Converts the timer's delta to a formatted string.
80
+ def to_s
81
+ min = (self.delta / 60).to_i
82
+ sec = (self.delta).to_i
83
+ msec = (self.delta * 100).to_i
84
+
85
+ "#{min}:#{sec}:#{msec}"
86
+ end
87
+
88
+ end
89
+
@@ -0,0 +1,203 @@
1
+
2
+ require_relative 'screen.rb'
3
+ require_relative 'timer.rb'
4
+
5
+ # A full guitar tab, as shown on the screen.
6
+ # Note that it depends on an already-existing window to exist.
7
+ #
8
+ class Track
9
+ COMMENT_CHAR = '#'
10
+ attr_reader :screen, :percent_completed
11
+ attr_accessor :speed
12
+
13
+ # Creates a Track that will be shown on `screen`.
14
+ # See Screen.
15
+ def initialize(screen)
16
+ @offset = 0
17
+ @timer = Timer.new
18
+ @timer.start
19
+ @speed = 0
20
+ @screen = screen
21
+ @percent_completed = 0
22
+
23
+ @raw_track = []
24
+ @raw_track[0] = ""
25
+ @raw_track[1] = ""
26
+ @raw_track[2] = ""
27
+ @raw_track[3] = ""
28
+ @raw_track[4] = ""
29
+ @raw_track[5] = ""
30
+ @raw_track[6] = ""
31
+ end
32
+
33
+ # Loads and parses +filename+'s contents into Track.
34
+ def load filename
35
+ if not File.exist? filename
36
+ raise "Error: File '#{filename}' doesn't exist!"
37
+ end
38
+ if not File.file? filename
39
+ raise "Error: '#{filename}' is not a file!"
40
+ end
41
+
42
+ file = File.new filename
43
+
44
+ # The thing here is there's no way I can know in
45
+ # advance how many lines the tab track will have.
46
+ #
47
+ # People put lots of strange things on them like
48
+ # timing, comments, etecetera.
49
+ #
50
+ # So I will read all non-blank lines, creating a
51
+ # counter. Then I will use it to display the track
52
+ # onscreen.
53
+ #
54
+ # I will also make every line have the same width
55
+ # as of the biggest one.
56
+
57
+ # Any tab line MUST have EITHER ---1---9--| OR |---3----0
58
+ tab_line = /[-[:alnum:]]\||\|[-[:alnum:]]/
59
+
60
+ # Duration of each note only has those chars.
61
+ # So we look for anything BUT these chars.
62
+ not_duration_line = /[^WHQESTX \.]/
63
+
64
+ count = 0
65
+ max_width = 0
66
+
67
+ file.readlines.each do |line|
68
+ next if line[0] == COMMENT_CHAR
69
+
70
+ line.chomp!
71
+ if line.empty?
72
+
73
+ # Making sure everything will have the same width
74
+ @raw_track.each_with_index do |t, i|
75
+ if t.length < max_width
76
+ @raw_track[i] += (' ' * (max_width - t.length))
77
+ end
78
+ end
79
+
80
+ count = 0
81
+ max_width = 0
82
+
83
+ # Lines must be EITHER a tab_line OR a duration_line.
84
+ # not not duration line means that
85
+ # (I should find a better way of expressing myself on regexes)
86
+ elsif (line =~ tab_line) or (not line =~ not_duration_line)
87
+ @raw_track[count] += line
88
+
89
+ if @raw_track[count].length > max_width
90
+ max_width = @raw_track[count].length
91
+ end
92
+
93
+ count += 1
94
+
95
+ end # Ignoring any other kind of line
96
+
97
+ if count > 7
98
+ raise "Error: Invalid format on '#{filename}'"
99
+ end
100
+
101
+ end
102
+ end
103
+
104
+ # Prints the track on the screen, along with string indicators
105
+ # on the left.
106
+ #
107
+ # It is shown at the vertical center of the provided Screen,
108
+ # spanning it's whole width.
109
+ def show
110
+ x = 1
111
+ y = (@screen.height/2) - (@raw_track.size/2)
112
+
113
+ # This both prints EADGBE and clears the whole screen,
114
+ # printing spaces where the track was.
115
+ #
116
+ # Also, if we have only 5 tracks, we leave the sixth
117
+ # indicator out of the screen.
118
+ if not @raw_track[6] =~ /[:blank:]/
119
+ @screen.mvaddstr(0, y, "E" + (' ' * (@screen.width - 1)))
120
+ @screen.mvaddstr(0, y + 1, "B" + (' ' * (@screen.width - 1)))
121
+ @screen.mvaddstr(0, y + 2, "G" + (' ' * (@screen.width - 1)))
122
+ @screen.mvaddstr(0, y + 3, "D" + (' ' * (@screen.width - 1)))
123
+ @screen.mvaddstr(0, y + 4, "A" + (' ' * (@screen.width - 1)))
124
+ @screen.mvaddstr(0, y + 5, "E" + (' ' * (@screen.width - 1)))
125
+ else
126
+ @screen.mvaddstr(0, y, ' ' * @screen.width)
127
+ @screen.mvaddstr(0, y + 1, "E" + (' ' * (@screen.width - 1)))
128
+ @screen.mvaddstr(0, y + 2, "B" + (' ' * (@screen.width - 1)))
129
+ @screen.mvaddstr(0, y + 3, "G" + (' ' * (@screen.width - 1)))
130
+ @screen.mvaddstr(0, y + 4, "D" + (' ' * (@screen.width - 1)))
131
+ @screen.mvaddstr(0, y + 5, "A" + (' ' * (@screen.width - 1)))
132
+ @screen.mvaddstr(0, y + 6, "E" + (' ' * (@screen.width - 1)))
133
+ end
134
+
135
+ (0...@raw_track.size).each do |i|
136
+ str = @raw_track[i]
137
+ str = str[@offset..(@offset + @screen.width - 2)]
138
+ @screen.mvaddstr(x, y + i, str)
139
+ end
140
+ end
141
+
142
+ # Scrolls the guitar tab by `n`.
143
+ #
144
+ # * If `n` is positive, scroll forward.
145
+ # * If `n` is negative, scroll backward.
146
+ def scroll n
147
+ @offset += n
148
+
149
+ left_limit = 0
150
+ right_limit = (@raw_track[0].length - @screen.width + 1).abs
151
+
152
+ if @offset < left_limit then @offset = left_limit end
153
+ if @offset > right_limit then @offset = right_limit end
154
+ end
155
+
156
+ # Goes to the beginning of the Track.
157
+ def begin
158
+ @offset = 0
159
+ @speed = 0
160
+ end
161
+
162
+ # Goes to the end of the Track.
163
+ def end
164
+ @offset = (@raw_track[0].length - @screen.width + 1).abs
165
+ @speed = 0
166
+ end
167
+
168
+ # Turns on/off Track's auto scroll functionality.
169
+ #
170
+ # Note that it won't work anyways if you don't keep calling
171
+ # +update+ method.
172
+ def auto_scroll option
173
+ if option == true
174
+ @timer.start if not @timer.running?
175
+ else
176
+ @timer.stop
177
+ end
178
+ end
179
+
180
+ # Updates Track's auto scroll functionality.
181
+ def update
182
+ return if not @timer.running?
183
+
184
+ current_completed = @offset + @screen.width - 1
185
+ @percent_completed = ((100.0 * current_completed)/@raw_track[0].length).ceil
186
+
187
+ if @timer.running? and @speed != 0
188
+ if @timer.delta > (1/(@speed*0.5)).abs
189
+ if @speed > 0
190
+ self.scroll 1
191
+ self.show
192
+ else
193
+ self.scroll -1
194
+ self.show
195
+ end
196
+
197
+ @timer.restart
198
+ end
199
+ end
200
+ end
201
+
202
+ end
203
+
data/lib/tabscroll.rb ADDED
@@ -0,0 +1,40 @@
1
+ # The main executable file.
2
+
3
+ require_relative 'tabscroll/engine'
4
+ require_relative 'tabscroll/screen'
5
+ require_relative 'tabscroll/track'
6
+ require_relative 'tabscroll/popup'
7
+
8
+ # Global vars
9
+ $engine = nil
10
+ PROGRAM_NAME = "tabscroll"
11
+ PROGRAM_VERSION = "0.0.1"
12
+
13
+ # Terminates program's execution normally, finishing the engine.
14
+ def quit
15
+ $engine.exit
16
+ exit 0
17
+ end
18
+
19
+ # Displays a help window, waiting for a keypress.
20
+ def show_help_window
21
+ title = 'Help'
22
+ text = <<END_OF_TEXT
23
+ q quit
24
+ h help/go back
25
+ left/right auto-scroll left/right
26
+ up/down stop auto-scrolling
27
+ </> step scroll left/right
28
+ o toggle status/title bars
29
+
30
+
31
+ #{PROGRAM_NAME} v#{PROGRAM_VERSION}
32
+ homepage alexdantas.net/projects/tabscroll
33
+ author Alexandre Dantas <eu@alexdantas.net>
34
+ END_OF_TEXT
35
+
36
+ pop = Popup.new(title, text)
37
+ will_quit = pop.show
38
+ quit if will_quit
39
+ end
40
+
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tabscroll
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexandre Dantas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-10-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Scrolls a textual guitar tab on the terminal.
14
+ email:
15
+ - eu@alexdantas.net
16
+ executables:
17
+ - tabscroll
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/tabscroll.rb
22
+ - lib/tabscroll/engine.rb
23
+ - lib/tabscroll/screen.rb
24
+ - lib/tabscroll/track.rb
25
+ - lib/tabscroll/popup.rb
26
+ - lib/tabscroll/timer.rb
27
+ - bin/tabscroll
28
+ homepage: http://www.alexdantas.net/projects/tabscroll
29
+ licenses:
30
+ - GPL-3.0
31
+ metadata: {}
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubyforge_project:
48
+ rubygems_version: 2.1.7
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Guitar tab scroller on the terminal
52
+ test_files: []