interactive_term 0.1.0

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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/interactive_term.rb +232 -0
  3. metadata +44 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a61881d188fff01d484a2d0051847690cb6c72e6
4
+ data.tar.gz: 90cb6d3515e959da4836d3e57d562b915cd12f9d
5
+ SHA512:
6
+ metadata.gz: 5ca96274e08bc0f97592020c112bd8dd6c5dcdbd2309095ff11d4d1b962109f76cc7fb8b3c638bbf059cd3551becb6c6bd6c434ab43a4cbfb371bfe797ce6667
7
+ data.tar.gz: dd865b869f4452eaab6c592039b757e233f090a5c0f64bfdc62fbfd3ee5475ee0062a8335e960f189a605a4475fc6d801d305a9a43f541609a1e0828a2f55d92
@@ -0,0 +1,232 @@
1
+ require 'thread'
2
+ require 'io/console'
3
+
4
+ module InteractiveTerm
5
+ class Term
6
+ FPS = 30
7
+ attr_reader :width, :height, :screen
8
+
9
+ def initialize(start_session = false)
10
+ @stty_state = @width = @height = @listener_thread = @session_active = @loop_active = nil
11
+ @listeners = []
12
+ @keypress_queue = Queue.new
13
+ @height, @width = IO.console.winsize
14
+
15
+ @screen = VirtualScreen.new(@width , @height)
16
+
17
+ self.start_session if start_session
18
+ end
19
+
20
+ def register_listener(&block)
21
+ @listeners << block
22
+ end
23
+
24
+ def start_session
25
+ @session_active = true
26
+
27
+ # switch to a new terminal context
28
+ system('tput smcup')
29
+
30
+ # hide the cursor
31
+ puts "\e[?25l"
32
+
33
+ # clear the screen
34
+ puts "\e[H\e[2J"
35
+
36
+ # store the stty state
37
+ @stty_state = `stty -g`
38
+
39
+ # raw: keypresses get passed along unprocessed
40
+ # -echo: user doesn't see what they type
41
+ # -icanon: no buffering/delay on keypress
42
+ # isig: enable quit special character (necessary because of raw)
43
+ `stty raw -echo -icanon isig`
44
+
45
+ @lisen_thread = Thread.new do
46
+ Thread.current.abort_on_exception = true
47
+ loop do
48
+ @keypress_queue.push $stdin.getc
49
+ end
50
+ end
51
+
52
+ trap("SIGINT") do
53
+ self.end_session
54
+ end
55
+ end
56
+
57
+ def loop(&block)
58
+ @loop_active = true
59
+
60
+ while @loop_active
61
+ # process up to 5 keypresses (should be fine because happens FPS times per second)
62
+ begin
63
+ 5.times do
64
+ keypress = @keypress_queue.pop(true) #nonblock
65
+ @listeners.each {|listener| listener.call(keypress)} if keypress
66
+ @keypress_queue = Queue.new
67
+ end
68
+ rescue ThreadError # nothing to pop
69
+ end
70
+
71
+ # run loop code
72
+ yield
73
+
74
+ @screen.update!
75
+
76
+ sleep 1.0/FPS
77
+ end
78
+ end
79
+
80
+ def break_loop
81
+ @loop_active = false
82
+ end
83
+
84
+ def end_session
85
+ #bring back the cursor
86
+ puts "\e[?25h"
87
+
88
+ #restore stty
89
+ `stty #{@stty_state}`
90
+
91
+ #return to original terminal context
92
+ system('tput rmcup')
93
+
94
+ @session_active = false
95
+ @loop_active = false
96
+ end
97
+
98
+ def debug!
99
+ unless @debug
100
+ @listeners << proc {|key| @screen.draw(key, width - 1, height - 1)}
101
+ @debug = true
102
+ end
103
+ end
104
+ end
105
+
106
+ # TODO: Bitmap should accept array of strings as well as array of array of chars (length one strings)
107
+ class Bitmap
108
+ def self.iterate(bitmap, &block)
109
+ y = 0
110
+ bitmap.each do |str|
111
+ x = 0
112
+ str.each_char do |char|
113
+ yield char, x, y
114
+ x += 1
115
+ end
116
+ y += 1
117
+ end
118
+ end
119
+ end
120
+
121
+ class Sprite
122
+ attr_accessor :x, :y, :bitmap
123
+
124
+ # bitmap is an array of strings.
125
+ # (x,y) is the position of the first character.
126
+ # each string represents the next line.
127
+ # newlines in bitmap is undefined behavior.
128
+ def initialize(x, y, bitmap)
129
+ @x = x
130
+ @y = y
131
+ @bitmap = bitmap
132
+ end
133
+
134
+ def set_screen_dimensions(screen_width, screen_height)
135
+ @screen_width = screen_width
136
+ @screen_height = screen_height
137
+ end
138
+
139
+ def iterate(&block)
140
+ Bitmap.iterate(@bitmap, &block)
141
+ end
142
+
143
+ def width
144
+ @width || @bitmap.first.size
145
+ end
146
+
147
+ def height
148
+ @height || @bitmap.size
149
+ end
150
+ end
151
+
152
+ class VirtualScreen
153
+ attr_reader :sprites, :width, :height
154
+
155
+ def initialize(width, height)
156
+ @width = width
157
+ @height = height
158
+
159
+ @sprites = []
160
+
161
+ @draw_mutex = Mutex.new
162
+
163
+ @screen_buffer = ScreenBuffer.new(@width, @height)
164
+ end
165
+
166
+ def add_sprite(sprite)
167
+ # might want to simply give sprite a reference to screen
168
+ sprite.set_screen_dimensions(@width, @height)
169
+ @sprites << sprite
170
+ end
171
+
172
+ def update!
173
+ new_buffer = ScreenBuffer.new(@width, @height)
174
+ new_buffer.render(sprites)
175
+ deltas = @screen_buffer.deltas(new_buffer)
176
+ deltas.each {|d| draw(*d)}
177
+ @screen_buffer = new_buffer
178
+ end
179
+
180
+ def draw(char, x, y)
181
+ @draw_mutex.synchronize do
182
+ print "\e[#{y+1};#{x+1}H#{char}"
183
+ end
184
+ end
185
+
186
+ def cleanup
187
+ @draw_mutex.unlock if @draw_mutex.locked?
188
+ #TODO: might want to do a full clean up of screen here
189
+ end
190
+ end
191
+
192
+ class ScreenBuffer
193
+ attr_reader :buffer
194
+
195
+ def initialize(width, height)
196
+ @width = width
197
+ @height = height
198
+ # There might be a reason to use an actual noop character instead of space
199
+ @buffer = @height.times.map { @width.times.map {" "}}
200
+ end
201
+
202
+ def render(sprites)
203
+ sprites.each do |sprite|; x_pos = sprite.x; y_pos = sprite.y;
204
+ sprite.iterate do |char, x, y|
205
+ matrix_safe_insert(@buffer, char, x_pos + x, y_pos + y)
206
+ end
207
+ end
208
+ end
209
+
210
+ # Inserts the given char at x, y of matrix
211
+ # Return without doing anything if x y is out of bounds
212
+ def matrix_safe_insert(matrix, char, x, y)
213
+ return if x < 0 || x >= matrix.first.size
214
+ return if y < 0 || y >= matrix.size
215
+ matrix[y][x] = char
216
+ end
217
+
218
+ def pretty_print
219
+ @height.times {|y| @width.times {|x| print @buffer[y][x]}; puts nil}
220
+ end
221
+
222
+ # Returns the character from other_screen_buffer when a delta is found, so this is not commutative
223
+ # Assumes other_screen_buffer is the same size as @buffer
224
+ def deltas(other_screen_buffer)
225
+ deltas = []
226
+ @height.times do |y|; @width.times do |x|;
227
+ deltas << [other_screen_buffer.buffer[y][x], x, y] if @buffer[y][x] != other_screen_buffer.buffer[y][x]
228
+ end; end;
229
+ deltas
230
+ end
231
+ end
232
+ end
metadata ADDED
@@ -0,0 +1,44 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interactive_term
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Ciollaro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: cciollaro@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/interactive_term.rb
20
+ homepage: https://github.com/cciollaro/interactive_term
21
+ licenses:
22
+ - MIT
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubyforge_project:
40
+ rubygems_version: 2.4.5
41
+ signing_key:
42
+ specification_version: 4
43
+ summary: Ruby API for interactive terminal apps!
44
+ test_files: []