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.
- checksums.yaml +7 -0
- data/lib/interactive_term.rb +232 -0
- 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: []
|