termcontroller 0.1.0 → 0.3
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/.gitignore +1 -1
- data/Gemfile +1 -0
- data/README.md +50 -7
- data/lib/termcontroller/controller.rb +290 -0
- data/lib/termcontroller/version.rb +1 -1
- data/lib/termcontroller.rb +1 -1
- data/termcontroller.gemspec +1 -0
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 137ab6e058cbc71d9ac66d57479fcb01a9ab87fb403c795c553c880498c21410
|
4
|
+
data.tar.gz: b51e7f7353b269fc8e0375d858e9c0ee8605ea0e340b1c4f000ec4557e6ac281
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a1d7893783340263e8d92ed7503ef239b77407e4466305f2fbbb1700b73108bb34d23eb34f8fcb3f8680bf5ac40f191cce9eba7ff8d3188d4213c0885c04d16
|
7
|
+
data.tar.gz: a6270a9cb7c27ee6744540013e6abc599851f846cc5b8b1a76bc08f34a4bc1784cf60ef89e999e588d653afa38f3e407f3d12c9ae4bfddf61e01529e8d5638ce
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
# Termcontroller
|
2
2
|
|
3
|
-
|
3
|
+
A very basic Controller (in the MVC sense) for Ruby terminal
|
4
|
+
applications. This was pulled out of my text editor,
|
5
|
+
[Re](https://github.com/vidarh/re).
|
4
6
|
|
5
|
-
|
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.
|
6
12
|
|
7
13
|
## Installation
|
8
14
|
|
@@ -22,19 +28,56 @@ 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
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
64
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
65
|
+
Then, run `rake spec` to run the tests. You can also run `bin/console`
|
66
|
+
for an interactive prompt that will allow you to experiment.
|
30
67
|
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake
|
68
|
+
To install this gem onto your local machine, run `bundle exec rake
|
69
|
+
install`. To release a new version, update the version number in
|
70
|
+
`version.rb`, and then run `bundle exec rake release`, which will create
|
71
|
+
a git tag for the version, push git commits and tags, and push the `.gem`
|
72
|
+
file to [rubygems.org](https://rubygems.org).
|
32
73
|
|
33
74
|
## Contributing
|
34
75
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at
|
76
|
+
Bug reports and pull requests are welcome on GitHub at
|
77
|
+
https://github.com/vidarh/termcontroller.
|
36
78
|
|
37
79
|
|
38
80
|
## License
|
39
81
|
|
40
|
-
The gem is available as open source under the terms of the [MIT
|
82
|
+
The gem is available as open source under the terms of the [MIT
|
83
|
+
License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,290 @@
|
|
1
|
+
# coding utf-8
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
require 'fcntl'
|
5
|
+
|
6
|
+
# # Controller
|
7
|
+
#
|
8
|
+
# This is way too ill defined. The purpose is to be able to have a
|
9
|
+
# separate thread handle the keyboard processing asynchronously,
|
10
|
+
# reading from the input, and for an application to then be able to
|
11
|
+
# call into it to read a command, or to have the controller dispatch
|
12
|
+
# the command directly to a specified target object based on the
|
13
|
+
# key bindings.
|
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
|
+
#
|
23
|
+
# It works well enough to e.g. allow temporarily pausing the processing
|
24
|
+
# and then dispatching "binding.pry" and re-enable the processing when
|
25
|
+
# it returns.
|
26
|
+
#
|
27
|
+
# That said there are lots of hacks in here that needs to be cleaned up
|
28
|
+
#
|
29
|
+
# FIXME: Should probably treat this as a singleton.
|
30
|
+
#
|
31
|
+
#
|
32
|
+
|
33
|
+
require 'keyboard_map'
|
34
|
+
|
35
|
+
module Termcontroller
|
36
|
+
DOUBLE_CLICK_INTERVAL=0.5
|
37
|
+
|
38
|
+
class Controller
|
39
|
+
@@controllers = []
|
40
|
+
|
41
|
+
attr_reader :lastcmd, :commands
|
42
|
+
|
43
|
+
def initialize(target=nil, keybindings={})
|
44
|
+
@m = Mutex.new
|
45
|
+
|
46
|
+
@target = target
|
47
|
+
@target_stack = []
|
48
|
+
|
49
|
+
@keybindings = keybindings
|
50
|
+
@buf = ""
|
51
|
+
@commands = Queue.new
|
52
|
+
@mode = :cooked
|
53
|
+
|
54
|
+
@kb = KeyboardMap.new
|
55
|
+
@con = IO.console
|
56
|
+
raise if !@con
|
57
|
+
|
58
|
+
at_exit { quit }
|
59
|
+
trap("CONT") { resume }
|
60
|
+
trap("WINCH") { @commands << :resize }
|
61
|
+
|
62
|
+
setup
|
63
|
+
|
64
|
+
@t = Thread.new { readloop }
|
65
|
+
|
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
|
+
setup
|
97
|
+
r
|
98
|
+
rescue Interrupt
|
99
|
+
ensure
|
100
|
+
@mode = old
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
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
|
118
|
+
|
119
|
+
def handle_input
|
120
|
+
if c = @commands.pop
|
121
|
+
do_command(c)
|
122
|
+
end
|
123
|
+
return Array(c)
|
124
|
+
end
|
125
|
+
|
126
|
+
def suspend
|
127
|
+
pause do
|
128
|
+
yield if block_given?
|
129
|
+
Process.kill("STOP", 0)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def resume
|
134
|
+
@mode = :cooked
|
135
|
+
setup
|
136
|
+
@commands << [:resume]
|
137
|
+
end
|
138
|
+
|
139
|
+
private # # ########################################################
|
140
|
+
|
141
|
+
def setup
|
142
|
+
STDOUT.print "\e[?2004h" # Enable bracketed paste
|
143
|
+
STDOUT.print "\e[?1000h" # Enable mouse reporting
|
144
|
+
STDOUT.print "\e[?1002h" # Enable mouse *move when clicked* reporting
|
145
|
+
STDOUT.print "\e[?1006h" # Enable extended reporting
|
146
|
+
STDOUT.print "\e[?25l" # Hide cursor
|
147
|
+
@con.raw!
|
148
|
+
end
|
149
|
+
|
150
|
+
def cleanup
|
151
|
+
# Some programs, like more and docker struggles if the terminal
|
152
|
+
# is not returned to blocking mode
|
153
|
+
stdin_flags = STDIN.fcntl(Fcntl::F_GETFL)
|
154
|
+
STDIN.fcntl(Fcntl::F_SETFL, stdin_flags & ~Fcntl::O_NONBLOCK) #
|
155
|
+
|
156
|
+
@con.cooked!
|
157
|
+
STDOUT.print "\e[?2004l" #Disable bracketed paste
|
158
|
+
STDOUT.print "\e[?1000l" #Disable mouse reporting
|
159
|
+
STDOUT.print "\e[?25h" # Show cursor
|
160
|
+
end
|
161
|
+
|
162
|
+
def quit
|
163
|
+
@@controllers.each {|t| t.kill }
|
164
|
+
cleanup
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
def fill_buf
|
169
|
+
# We do this to ensure the other thread gets a chance
|
170
|
+
sleep(0.01)
|
171
|
+
|
172
|
+
@m.synchronize do
|
173
|
+
@con.raw!
|
174
|
+
return if !IO.select([$stdin],nil,nil,0.1)
|
175
|
+
str = $stdin.read_nonblock(4096)
|
176
|
+
|
177
|
+
# FIXME...
|
178
|
+
str.force_encoding("utf-8")
|
179
|
+
@buf << str
|
180
|
+
end
|
181
|
+
rescue IO::WaitReadable
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
def getc
|
186
|
+
while @buf.empty?
|
187
|
+
fill_buf
|
188
|
+
end
|
189
|
+
@buf.slice!(0) if !paused? && @mode == :cooked
|
190
|
+
rescue Interrupt
|
191
|
+
end
|
192
|
+
|
193
|
+
# We do this because @keybindings can be changed
|
194
|
+
# on the main thread if the client changes the bindings,
|
195
|
+
# e.g. by calling `#raw`. This is the *only* sanctioned
|
196
|
+
# way to look up the binding.
|
197
|
+
#
|
198
|
+
def map(key, map = @keybindings)
|
199
|
+
map && key ? map[key.to_sym] : nil
|
200
|
+
end
|
201
|
+
|
202
|
+
def get_command
|
203
|
+
# Keep track of compound mapping
|
204
|
+
cmap = nil
|
205
|
+
cmdstr = ""
|
206
|
+
|
207
|
+
loop do
|
208
|
+
c = nil
|
209
|
+
char = getc
|
210
|
+
return nil if !char
|
211
|
+
|
212
|
+
a = Array(@kb.call(char))
|
213
|
+
c1 = a.first
|
214
|
+
|
215
|
+
if c1 == :mouse_up
|
216
|
+
t = Time.now
|
217
|
+
dt = @lastclick ? t - @lastclick : 999
|
218
|
+
if dt < DOUBLE_CLICK_INTERVAL
|
219
|
+
c1 = :mouse_doubleclick
|
220
|
+
else
|
221
|
+
c1 = :mouse_click
|
222
|
+
end
|
223
|
+
@lastclick = t
|
224
|
+
end
|
225
|
+
|
226
|
+
c = map(c1, cmap || @keybindings)
|
227
|
+
|
228
|
+
if c.nil? && c1.kind_of?(String)
|
229
|
+
return [:char, c1]
|
230
|
+
end
|
231
|
+
|
232
|
+
if c.nil?
|
233
|
+
if c1
|
234
|
+
args = c1.respond_to?(:args) ? c1.args : []
|
235
|
+
@lastcmd = cmdstr + c1.to_sym.to_s
|
236
|
+
return Array(c1.to_sym).concat(args || [])
|
237
|
+
else
|
238
|
+
@lastcmd = cmdstr + char.inspect
|
239
|
+
return nil
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
str = c1.to_sym.to_s.split("_").join(" ")
|
244
|
+
if cmdstr.empty?
|
245
|
+
cmdstr = str
|
246
|
+
else
|
247
|
+
cmdstr += " + " + str
|
248
|
+
end
|
249
|
+
|
250
|
+
if c.kind_of?(Hash) # Compound mapping
|
251
|
+
cmap = c
|
252
|
+
else
|
253
|
+
@lastcmd = cmdstr + " (#{c.to_s})" if c.to_s != @lastcmd
|
254
|
+
return c
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def read_input
|
260
|
+
c = get_command
|
261
|
+
if !c
|
262
|
+
Thread.pass
|
263
|
+
return
|
264
|
+
end
|
265
|
+
|
266
|
+
@commands << c
|
267
|
+
end
|
268
|
+
|
269
|
+
def readloop
|
270
|
+
loop do
|
271
|
+
if @mode == :cooked
|
272
|
+
read_input
|
273
|
+
else
|
274
|
+
fill_buf
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def do_command(c)
|
280
|
+
return nil if !c || !@target
|
281
|
+
a = Array(c)
|
282
|
+
if @target.respond_to?(a.first)
|
283
|
+
@target.instance_eval { send(*a) }
|
284
|
+
else
|
285
|
+
@lastcmd = "Unbound: #{a.first.inspect}"
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
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.3'
|
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: 2023-08-02 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
|
@@ -27,6 +41,7 @@ files:
|
|
27
41
|
- bin/console
|
28
42
|
- bin/setup
|
29
43
|
- lib/termcontroller.rb
|
44
|
+
- lib/termcontroller/controller.rb
|
30
45
|
- lib/termcontroller/version.rb
|
31
46
|
- termcontroller.gemspec
|
32
47
|
homepage: https://github.com/vidarh/termcontroller
|