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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1fad37d234332ef83aa87356c2ce2cb16f0625bc5e53e37db193e2d0f0343fd
4
- data.tar.gz: cad01ac6503432df902fe0c1d204c6698d13088376e23a418c1d7ca5d5709cab
3
+ metadata.gz: 137ab6e058cbc71d9ac66d57479fcb01a9ab87fb403c795c553c880498c21410
4
+ data.tar.gz: b51e7f7353b269fc8e0375d858e9c0ee8605ea0e340b1c4f000ec4557e6ac281
5
5
  SHA512:
6
- metadata.gz: b0d4ba2609f21e798a4ea6798d02f94f7dba23f6011d015568206c219dbccedb8b994c6d66e30fc4b25c6e4ab9c56b7455f12aa1dec856ba5dac4eddcae77e4e
7
- data.tar.gz: 7d4b1b9a57cc196538851e537403e2ff8bafbfeab55d22ad3fccbf70ad27c4df22bc0ac99701c1ffa587af02baeb62f2ac088527bd1655a26f39d7cc9cdee01d
6
+ metadata.gz: 4a1d7893783340263e8d92ed7503ef239b77407e4466305f2fbbb1700b73108bb34d23eb34f8fcb3f8680bf5ac40f191cce9eba7ff8d3188d4213c0885c04d16
7
+ data.tar.gz: a6270a9cb7c27ee6744540013e6abc599851f846cc5b8b1a76bc08f34a4bc1784cf60ef89e999e588d653afa38f3e407f3d12c9ae4bfddf61e01529e8d5638ce
data/.gitignore CHANGED
@@ -6,6 +6,6 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
-
9
+ *~
10
10
  # rspec failure tracking
11
11
  .rspec_status
data/Gemfile CHANGED
@@ -3,5 +3,6 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in termcontroller.gemspec
4
4
  gemspec
5
5
 
6
+ gem "keyboard_map" #, path: "../keyboard_map"
6
7
  gem "rake", "~> 12.0"
7
8
  gem "rspec", "~> 3.0"
data/README.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # Termcontroller
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/termcontroller`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
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
- TODO: Write usage instructions here
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. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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 install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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 https://github.com/[USERNAME]/termcontroller.
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 License](https://opensource.org/licenses/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
@@ -1,3 +1,3 @@
1
1
  module Termcontroller
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3"
3
3
  end
@@ -1,6 +1,6 @@
1
1
  require "termcontroller/version"
2
+ require "termcontroller/controller"
2
3
 
3
4
  module Termcontroller
4
5
  class Error < StandardError; end
5
- # Your code goes here...
6
6
  end
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.bindir = "exe"
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
+ spec.add_dependency "keyboard_map", ">=0.1.3"
29
30
  end
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.1.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: 2021-11-17 00:00:00.000000000 Z
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