termcontroller 0.2 → 0.5
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 +4 -4
- data/Gemfile +1 -0
- data/README.md +36 -1
- data/examples/draw.rb +57 -0
- data/lib/termcontroller/controller.rb +238 -188
- data/lib/termcontroller/version.rb +1 -1
- data/lib/termcontroller.rb +0 -1
- data/termcontroller.gemspec +1 -0
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04632d599e81a07a9dcda4090091356de0fe257b01342d31719937fd366ccf8f
|
4
|
+
data.tar.gz: 8f411109feb2e00122c80bb7be2978a19e4e1af94f131b66ff6a25476fd1eb99
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b693ada8e7fab935972a7ad40944eac7da728f5292f81b7217c94839dd0ff06896f909ca13ed0dddc630258020d7fa0ed3c9e4eba36efe430b679a962526b68
|
7
|
+
data.tar.gz: 5b189f18f46ed86916cef9fedbc7b70766c3fe3d4d229c22c4f704c1b37e05610b2ed8396b324218ed865ba0f8a23ddb34b178a2eba1f80aa61728f06ac7a27f
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,6 +4,12 @@ A very basic Controller (in the MVC sense) for Ruby terminal
|
|
4
4
|
applications. This was pulled out of my text editor,
|
5
5
|
[Re](https://github.com/vidarh/re).
|
6
6
|
|
7
|
+
**YOU ALMOST CERTAINLY DO NOT YET WANT TO USE THIS FOR YOUR OWN CODE**
|
8
|
+
|
9
|
+
I do intend to clean it up and make it usable for others, but it's not
|
10
|
+
there yet - if you think it'd be useful for you, feel free to drop me a
|
11
|
+
message.
|
12
|
+
|
7
13
|
## Installation
|
8
14
|
|
9
15
|
Add this line to your application's Gemfile:
|
@@ -22,7 +28,36 @@ Or install it yourself as:
|
|
22
28
|
|
23
29
|
## Usage
|
24
30
|
|
25
|
-
|
31
|
+
```ruby
|
32
|
+
require 'termcontroller'
|
33
|
+
|
34
|
+
class Target
|
35
|
+
def initialize
|
36
|
+
# Second argument is a keyboard mapping.
|
37
|
+
@ctrl = Termcontroller::Controller.new(self, {
|
38
|
+
:f1 => :help
|
39
|
+
})
|
40
|
+
end
|
41
|
+
|
42
|
+
def help
|
43
|
+
puts "This is a help text"
|
44
|
+
end
|
45
|
+
|
46
|
+
def run
|
47
|
+
loop do
|
48
|
+
p @ctrl.handle_input
|
49
|
+
print "\r"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def ctrl_c; exit; end
|
54
|
+
end
|
55
|
+
|
56
|
+
puts "Ctrl+c to quit"
|
57
|
+
Target.new.run
|
58
|
+
```
|
59
|
+
|
60
|
+
|
26
61
|
|
27
62
|
## Development
|
28
63
|
|
data/examples/draw.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
$: << File.expand_path(File.dirname(__FILE__)+"/../lib/")
|
3
|
+
|
4
|
+
require 'termcontroller'
|
5
|
+
|
6
|
+
class Target
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
keymap = { :ctrl_c => :quit }
|
10
|
+
@ctrl = Termcontroller::Controller.new(self, keymap)
|
11
|
+
@c = " "
|
12
|
+
@col = 1
|
13
|
+
end
|
14
|
+
|
15
|
+
# If a method exists in the target class passed to Controller.new
|
16
|
+
# that matches the keymap, then it is called directly.
|
17
|
+
#
|
18
|
+
# In this case `quit` is defined in the map above,
|
19
|
+
# while `char` is a default.
|
20
|
+
#
|
21
|
+
# If nothing is mapped, then `handle_input` will return the
|
22
|
+
# symbol instead, as shown for `mouse_down`.
|
23
|
+
#
|
24
|
+
def quit = (@running = false)
|
25
|
+
def char(c) = (@c = c)
|
26
|
+
|
27
|
+
def call
|
28
|
+
@running = true
|
29
|
+
col = 1
|
30
|
+
while @running
|
31
|
+
cmd = @ctrl.handle_input
|
32
|
+
case cmd.first
|
33
|
+
when :mouse_down
|
34
|
+
print "\e[5;1H#{cmd.inspect}"
|
35
|
+
print "\e[#{cmd[3]};#{cmd[2]}H"
|
36
|
+
if cmd[1] & 3 == 0
|
37
|
+
# left
|
38
|
+
print "\e[4#{col}m#{@c}\e[49m"
|
39
|
+
print "\e[1;1H[DRAWING] "
|
40
|
+
elsif cmd[1] & 3 == 2
|
41
|
+
# right
|
42
|
+
print " "
|
43
|
+
print "\e[1;1H[ERASING] "
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
print "\e[2J" # Clear screen
|
52
|
+
print "\e[4;1HHold left button and move mouse to draw; Hold right button and move to clear"
|
53
|
+
print "\e[3;1HPress any letter to set a character to fill with on draw."
|
54
|
+
print "\e[2;1HCtrl+c to quit"
|
55
|
+
Target.new.call
|
56
|
+
print "\e[2J" # Clear screen
|
57
|
+
print "\e[1;1H" # Move top left
|
@@ -1,6 +1,7 @@
|
|
1
|
-
# coding
|
1
|
+
# coding utf-8
|
2
2
|
|
3
3
|
require 'io/console'
|
4
|
+
require 'fcntl'
|
4
5
|
|
5
6
|
# # Controller
|
6
7
|
#
|
@@ -11,248 +12,297 @@ require 'io/console'
|
|
11
12
|
# the command directly to a specified target object based on the
|
12
13
|
# key bindings.
|
13
14
|
#
|
15
|
+
# Note that *currently* `#handle_input` will block on retrieving a
|
16
|
+
# command with no timeout. This is because Re is the only current
|
17
|
+
# client of this class, and *currently* does not have a need for
|
18
|
+
# anything async. That *will* change, as Re will eventually start
|
19
|
+
# receiving evented updates. Current "hack" to work around this:
|
20
|
+
# push things into the `commands` queue. This may well be the ongoing
|
21
|
+
# way to do it, and I might consider breaking it out.
|
22
|
+
#
|
14
23
|
# It works well enough to e.g. allow temporarily pausing the processing
|
15
24
|
# and then dispatching "binding.pry" and re-enable the processing when
|
16
25
|
# it returns.
|
17
26
|
#
|
18
|
-
#
|
27
|
+
# That said there are lots of hacks in here that needs to be cleaned up
|
19
28
|
#
|
20
29
|
# FIXME: Should probably treat this as a singleton.
|
21
30
|
#
|
22
31
|
#
|
23
32
|
|
33
|
+
require 'keyboard_map'
|
34
|
+
|
24
35
|
module Termcontroller
|
36
|
+
DOUBLE_CLICK_INTERVAL=0.5
|
37
|
+
|
25
38
|
class Controller
|
39
|
+
@@controllers = []
|
26
40
|
|
27
|
-
attr_reader :lastcmd
|
28
|
-
attr_accessor :mode
|
29
|
-
|
30
|
-
@@con = IO.console
|
31
|
-
|
32
|
-
# Pause *any* Controller instance
|
33
|
-
@@pause = false
|
34
|
-
def self.pause!
|
35
|
-
old = @@pause
|
36
|
-
@@pause = true
|
37
|
-
@@con.cooked do
|
38
|
-
yield
|
39
|
-
end
|
40
|
-
ensure
|
41
|
-
@@pause = old
|
42
|
-
end
|
41
|
+
attr_reader :lastcmd, :commands
|
43
42
|
|
44
|
-
def
|
45
|
-
@
|
46
|
-
end
|
43
|
+
def initialize(target=nil, keybindings={})
|
44
|
+
@m = Mutex.new
|
47
45
|
|
48
|
-
def initialize(target, keybindings)
|
49
46
|
@target = target
|
47
|
+
@target_stack = []
|
48
|
+
|
50
49
|
@keybindings = keybindings
|
51
50
|
@buf = ""
|
52
|
-
@commands =
|
51
|
+
@commands = Queue.new
|
53
52
|
@mode = :cooked
|
54
53
|
|
55
54
|
@kb = KeyboardMap.new
|
56
|
-
|
55
|
+
@con = IO.console
|
57
56
|
raise if !@con
|
58
|
-
|
59
|
-
at_exit do
|
60
|
-
cleanup
|
61
|
-
end
|
62
|
-
|
63
|
-
trap("CONT") { resume }
|
64
57
|
|
65
|
-
|
66
|
-
|
58
|
+
at_exit { quit }
|
59
|
+
trap("CONT") { resume }
|
60
|
+
trap("WINCH") { @commands << :resize }
|
67
61
|
|
68
|
-
|
69
|
-
STDOUT.print "\e[?2004h" # Enable bracketed paste
|
70
|
-
STDOUT.print "\e[?1000h" # Enable mouse reporting
|
71
|
-
STDOUT.print "\e[?1006h" # Enable extended reporting
|
72
|
-
end
|
62
|
+
setup
|
73
63
|
|
74
|
-
|
75
|
-
STDOUT.print "\e[?2004l" #Disable bracketed paste
|
76
|
-
STDOUT.print "\e[?1000l" #Disable mouse reporting
|
77
|
-
end
|
64
|
+
@t = Thread.new { readloop }
|
78
65
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
66
|
+
@@controllers << @t
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
def paused?; @mode == :pause; end
|
71
|
+
|
72
|
+
def push_target(t)
|
73
|
+
@target_stack << @target
|
74
|
+
@target = t
|
75
|
+
end
|
76
|
+
|
77
|
+
def pop_target
|
78
|
+
t = @target
|
79
|
+
@target = @target_stack.pop
|
80
|
+
t
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
#
|
85
|
+
# Pause processing so you can read directly from stdin
|
86
|
+
# yourself. E.g. to use Readline, or to suspend
|
87
|
+
#
|
88
|
+
def pause
|
89
|
+
@m.synchronize do
|
90
|
+
old = @mode
|
91
|
+
begin
|
92
|
+
@mode = :pause
|
93
|
+
IO.console.cooked!
|
94
|
+
cleanup
|
95
|
+
r = yield
|
96
|
+
r
|
97
|
+
rescue Interrupt
|
98
|
+
ensure
|
99
|
+
@mode = old
|
100
|
+
setup
|
101
|
+
end
|
87
102
|
end
|
88
103
|
end
|
89
|
-
end
|
90
104
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
105
|
+
# FIXME: The first event after the yield
|
106
|
+
# appears to fail to pass the mapping.
|
107
|
+
# Maybe move the mapping to the client thread?
|
108
|
+
def raw
|
109
|
+
keybindings = @keybindings
|
110
|
+
@keybindings = {}
|
111
|
+
push_target(nil)
|
112
|
+
yield
|
113
|
+
rescue Interrupt
|
114
|
+
ensure
|
115
|
+
@keybindings = keybindings
|
116
|
+
pop_target
|
117
|
+
end
|
101
118
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
return
|
119
|
+
def handle_input
|
120
|
+
if c = @commands.pop
|
121
|
+
do_command(c)
|
122
|
+
end
|
123
|
+
return Array(c)
|
107
124
|
end
|
108
|
-
@con.raw!
|
109
|
-
return if !IO.select([$stdin],nil,nil,0.1)
|
110
|
-
str = $stdin.read_nonblock(4096)
|
111
|
-
str.force_encoding("utf-8")
|
112
|
-
@buf << str
|
113
|
-
rescue IO::WaitReadable
|
114
|
-
end
|
115
125
|
|
116
|
-
|
117
|
-
if
|
118
|
-
|
119
|
-
|
126
|
+
# USE WITH CAUTION. This will pause processing,
|
127
|
+
# yield to the provided block if any, and then send
|
128
|
+
# SIGSTOP to the process.
|
129
|
+
#
|
130
|
+
# This is meant to allow for handling ctrl-z to
|
131
|
+
# suspend in processes that need to be able to
|
132
|
+
# reset the terminal to a better state before stopping.
|
133
|
+
#
|
134
|
+
# FIXME: It seems like it does not work as expected.
|
135
|
+
#
|
136
|
+
def suspend
|
137
|
+
pause do
|
138
|
+
yield if block_given?
|
139
|
+
Process.kill("STOP", 0)
|
120
140
|
end
|
121
|
-
@buf.slice!(0) if !paused? && @mode == :cooked
|
122
|
-
else
|
123
|
-
sleep(0.1)
|
124
|
-
Thread.pass
|
125
|
-
return nil
|
126
141
|
end
|
127
|
-
rescue Interrupt
|
128
|
-
end
|
129
142
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@mode = :cooked
|
136
|
-
end
|
143
|
+
def resume
|
144
|
+
@mode = :cooked
|
145
|
+
setup
|
146
|
+
@commands << [:resume]
|
147
|
+
end
|
137
148
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
rescue Interrupt
|
142
|
-
end
|
149
|
+
def hide_cursor
|
150
|
+
STDOUT.print "\e[?25l" # Hide cursor
|
151
|
+
end
|
143
152
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
c = nil
|
148
|
-
char = getc
|
149
|
-
return nil if !char
|
153
|
+
def show_cursor
|
154
|
+
STDOUT.print "\e[?25h" # Show cursor
|
155
|
+
end
|
150
156
|
|
151
|
-
|
152
|
-
c = map[c1.to_sym] if c1
|
157
|
+
private # # ########################################################
|
153
158
|
|
154
|
-
|
155
|
-
|
156
|
-
|
159
|
+
def setup
|
160
|
+
STDOUT.print "\e[?2004h" # Enable bracketed paste
|
161
|
+
STDOUT.print "\e[?1000h" # Enable mouse reporting
|
162
|
+
STDOUT.print "\e[?1002h" # Enable mouse *move when clicked* reporting
|
163
|
+
STDOUT.print "\e[?1006h" # Enable extended reporting
|
164
|
+
hide_cursor
|
165
|
+
@con.raw!
|
166
|
+
end
|
157
167
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
else
|
164
|
-
@lastchar = char.inspect
|
165
|
-
return nil
|
166
|
-
end
|
167
|
-
end
|
168
|
+
def cleanup
|
169
|
+
# Some programs, like more and docker struggles if the terminal
|
170
|
+
# is not returned to blocking mode
|
171
|
+
stdin_flags = STDIN.fcntl(Fcntl::F_GETFL)
|
172
|
+
STDIN.fcntl(Fcntl::F_SETFL, stdin_flags & ~Fcntl::O_NONBLOCK) #
|
168
173
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
@lastchar += " (#{c.to_s})" if c.to_s != @lastchar
|
174
|
-
return c
|
175
|
-
end
|
174
|
+
@con.cooked!
|
175
|
+
STDOUT.print "\e[?2004l" #Disable bracketed paste
|
176
|
+
STDOUT.print "\e[?1000l" #Disable mouse reporting
|
177
|
+
show_cursor
|
176
178
|
end
|
177
|
-
end
|
178
179
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
@lastcmd = c
|
183
|
-
@target.instance_eval { send(*Array(c)) }
|
184
|
-
else
|
185
|
-
@lastchar = "Unbound: #{Array(c).first.inspect}"
|
180
|
+
def quit
|
181
|
+
@@controllers.each {|t| t.kill }
|
182
|
+
cleanup
|
186
183
|
end
|
187
|
-
end
|
188
184
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
185
|
+
|
186
|
+
def fill_buf
|
187
|
+
# We do this to ensure the other thread gets a chance
|
188
|
+
sleep(0.01)
|
189
|
+
|
190
|
+
@m.synchronize do
|
191
|
+
@con.raw!
|
192
|
+
return if !IO.select([$stdin],nil,nil,0.1)
|
193
|
+
str = $stdin.read_nonblock(4096)
|
194
|
+
|
195
|
+
# FIXME...
|
196
|
+
str.force_encoding("utf-8")
|
197
|
+
@buf << str
|
198
|
+
end
|
199
|
+
rescue IO::WaitReadable
|
194
200
|
end
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
#return nil if !c
|
201
|
+
|
202
|
+
|
203
|
+
def getc
|
204
|
+
while @buf.empty?
|
205
|
+
fill_buf
|
206
|
+
end
|
207
|
+
@buf.slice!(0) if !paused? && @mode == :cooked
|
208
|
+
rescue Interrupt
|
204
209
|
end
|
205
|
-
@commands << c
|
206
|
-
Thread.pass
|
207
|
-
end
|
208
210
|
|
209
|
-
|
210
|
-
if
|
211
|
-
|
212
|
-
|
211
|
+
# We do this because @keybindings can be changed
|
212
|
+
# on the main thread if the client changes the bindings,
|
213
|
+
# e.g. by calling `#raw`. This is the *only* sanctioned
|
214
|
+
# way to look up the binding.
|
215
|
+
#
|
216
|
+
def map(key, map = @keybindings)
|
217
|
+
map && key ? map[key.to_sym] : nil
|
213
218
|
end
|
214
|
-
@commands.shift
|
215
|
-
end
|
216
219
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
+
def get_command
|
221
|
+
# Keep track of compound mapping
|
222
|
+
cmap = nil
|
223
|
+
cmdstr = ""
|
224
|
+
|
225
|
+
loop do
|
226
|
+
c = nil
|
227
|
+
char = getc
|
228
|
+
return nil if !char
|
229
|
+
|
230
|
+
a = Array(@kb.call(char))
|
231
|
+
c1 = a.first
|
232
|
+
|
233
|
+
if c1 == :mouse_up
|
234
|
+
t = Time.now
|
235
|
+
dt = @lastclick ? t - @lastclick : 999
|
236
|
+
if dt < DOUBLE_CLICK_INTERVAL
|
237
|
+
c1 = :mouse_doubleclick
|
238
|
+
else
|
239
|
+
c1 = :mouse_click
|
240
|
+
end
|
241
|
+
@lastclick = t
|
242
|
+
end
|
243
|
+
|
244
|
+
c = map(c1, cmap || @keybindings)
|
245
|
+
|
246
|
+
if c.nil? && c1.kind_of?(String)
|
247
|
+
return [:char, c1]
|
248
|
+
end
|
249
|
+
|
250
|
+
if c.nil?
|
251
|
+
if c1
|
252
|
+
args = c1.respond_to?(:args) ? c1.args : []
|
253
|
+
@lastcmd = cmdstr + c1.to_sym.to_s
|
254
|
+
return Array(c1.to_sym).concat(args || [])
|
255
|
+
else
|
256
|
+
@lastcmd = cmdstr + char.inspect
|
257
|
+
return nil
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
str = c1.to_sym.to_s.split("_").join(" ")
|
262
|
+
if cmdstr.empty?
|
263
|
+
cmdstr = str
|
264
|
+
else
|
265
|
+
cmdstr += " + " + str
|
266
|
+
end
|
267
|
+
|
268
|
+
if c.kind_of?(Hash) # Compound mapping
|
269
|
+
cmap = c
|
270
|
+
else
|
271
|
+
@lastcmd = cmdstr + " (#{c.to_s})" if c.to_s != @lastcmd
|
272
|
+
return c
|
273
|
+
end
|
274
|
+
end
|
220
275
|
end
|
221
|
-
return c
|
222
|
-
end
|
223
276
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
277
|
+
def read_input
|
278
|
+
c = get_command
|
279
|
+
if !c
|
280
|
+
Thread.pass
|
281
|
+
return
|
282
|
+
end
|
283
|
+
|
284
|
+
@commands << c
|
229
285
|
end
|
230
|
-
setup
|
231
|
-
end
|
232
286
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
287
|
+
def readloop
|
288
|
+
loop do
|
289
|
+
if @mode == :cooked
|
290
|
+
read_input
|
291
|
+
else
|
292
|
+
fill_buf
|
293
|
+
end
|
294
|
+
end
|
237
295
|
end
|
238
|
-
end
|
239
296
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
297
|
+
def do_command(c)
|
298
|
+
return nil if !c || !@target
|
299
|
+
a = Array(c)
|
300
|
+
if @target.respond_to?(a.first)
|
301
|
+
@target.instance_eval { send(*a) }
|
302
|
+
else
|
303
|
+
@lastcmd = "Unbound: #{a.first.inspect}"
|
304
|
+
end
|
305
|
+
end
|
246
306
|
|
247
|
-
at_exit do
|
248
|
-
#
|
249
|
-
# FIXME: This is a workaround for Controller putting
|
250
|
-
# STDIN into nonblocking mode and not cleaning up, which
|
251
|
-
# causes all kind of problems with a variety of tools (more,
|
252
|
-
# docker etc.) which expect it to be blocking.
|
253
|
-
Termcontroller::Controller.pause! do
|
254
|
-
stdin_flags = STDIN.fcntl(Fcntl::F_GETFL)
|
255
|
-
STDIN.fcntl(Fcntl::F_SETFL, stdin_flags & ~Fcntl::O_NONBLOCK) #
|
256
|
-
IO.console.cooked!
|
257
307
|
end
|
258
308
|
end
|
data/lib/termcontroller.rb
CHANGED
data/termcontroller.gemspec
CHANGED
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: termcontroller
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.5'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vidar Hokstad
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
11
|
+
date: 2025-06-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: keyboard_map
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.3
|
13
27
|
description:
|
14
28
|
email:
|
15
29
|
- vidar@hokstad.com
|
@@ -26,6 +40,7 @@ files:
|
|
26
40
|
- Rakefile
|
27
41
|
- bin/console
|
28
42
|
- bin/setup
|
43
|
+
- examples/draw.rb
|
29
44
|
- lib/termcontroller.rb
|
30
45
|
- lib/termcontroller/controller.rb
|
31
46
|
- lib/termcontroller/version.rb
|
@@ -50,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
65
|
- !ruby/object:Gem::Version
|
51
66
|
version: '0'
|
52
67
|
requirements: []
|
53
|
-
rubygems_version: 3.
|
68
|
+
rubygems_version: 3.4.10
|
54
69
|
signing_key:
|
55
70
|
specification_version: 4
|
56
71
|
summary: Controller/input processing for terminal applications
|