terminal_rb 0.17.2 → 0.19.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b65682d28124ce6b98d36529cf9eca06274f773c57e6282de858b7315ad4bccc
4
- data.tar.gz: 774c2f114d5fb2049226abbafd926b8bd5f5da8661f5a31182322ee015079c14
3
+ metadata.gz: 76294a5517d1c24b4c85f855cf802ce01dd1fa574dfac92753edd78b7a8fd465
4
+ data.tar.gz: 3e95e7b5ae8f40a9e284cdd3abddbd5a77a70e0a4df40e0deed2702acaf2ed07
5
5
  SHA512:
6
- metadata.gz: 53b711bfccc5b09faafe235a1ddf38ab422157fc95942e4be91e3cc0b4d1b35388a98f5f382cd206ae7435cc41499c4a44ef630b3a59d282d93e8b18569b1232
7
- data.tar.gz: 71138798500123c3aa36c5eb2b470150f392294828da4fb6bca972613228cc2b777aa4f4c9d0e5178798efd3bbbc3497c943445de651a51c2a03e2263a40bbe5
6
+ metadata.gz: 99e9e190caea636278031df7116ce0fdaf15fa6fdf2c6c1912906862341eae25e3b6aacc77296aca15b833e4548cbad36edf7f41160fc313059c6ecbe472fb2a
7
+ data.tar.gz: 0b5cbc5ef6ed87179d6b74663607bec29c07ad53ca4fc7c1593577214e31d5b08281fa68a119be361eb4ce720420767d09690b5679ee2f6e43ef3061c40b7ec7
data/README.md CHANGED
@@ -4,7 +4,7 @@ Terminal.rb supports you with input and output on your terminal. Simple [BBCode]
4
4
 
5
5
  - Gem: [rubygems.org](https://rubygems.org/gems/terminal_rb)
6
6
  - Source: [codeberg.org](https://codeberg.org/mblumtritt/Terminal.rb)
7
- - Help: [rubydoc.info](https://rubydoc.info/gems/terminal_rb/Terminal)
7
+ - Help: [rubydoc.info](https://rubydoc.info/gems/terminal_rb/0.19.0/Terminal)
8
8
 
9
9
  ## Features
10
10
 
@@ -60,7 +60,7 @@ TEXT
60
60
  # => "attribute syntax."]
61
61
  ```
62
62
 
63
- Have a look at the [examples](./examples/) directory to learn from code.
63
+ Have a look at the [examples](https://codeberg.org/mblumtritt/Terminal.rb/src/branch/main/examples) directory to learn from code.
64
64
 
65
65
  ## Installation
66
66
 
@@ -70,7 +70,7 @@ You can install the gem in your system with
70
70
  gem install terminal_rb
71
71
  ```
72
72
 
73
- or you can use [Bundler](http://gembundler.com/) to add Terminal.rb to your own project:
73
+ or you can use [Bundler](https://bundler.io) to add Terminal.rb to your own project:
74
74
 
75
75
  ```shell
76
76
  bundle add terminal_rb
data/bin/bbcode CHANGED
@@ -42,5 +42,5 @@ rescue IOError, SystemCallError => e
42
42
  $stderr.puts "#{me}-error: #{e.message.gsub(/@ \w+ /, '')}"
43
43
  exit 1
44
44
  rescue Interrupt
45
- exit 138
45
+ exit 130
46
46
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminal
4
+ module AnsiInput
5
+ attr_reader :input_mode
6
+
7
+ def on_key_event(mouse: false, mouse_move: false, focus: false)
8
+ raise('already reading key events') if (@in_recursion += 1) != 1
9
+ return unless block_given?
10
+ opts = __in_option(mouse, mouse_move, focus)
11
+ opts &&= @in.syswrite("#{opts}h") ? "#{opts}l" : nil
12
+ while (raw = @in.getch)
13
+ (yield(KeyEvent[raw]) ? next : break) if raw != "\e"
14
+ lesci = 0
15
+ while String === (nc = @in.read_nonblock(1, exception: false))
16
+ lesci = raw.size if nc == "\e"
17
+ raw << nc
18
+ end
19
+ (yield(KeyEvent[raw]) ? next : break) if lesci < 2
20
+ break unless raw[1..].split("\e").all? { yield(KeyEvent["\e#{_1}"]) }
21
+ end
22
+ rescue IOError, SystemCallError
23
+ __input_error
24
+ false
25
+ ensure
26
+ @in_recursion -= 1
27
+ @in&.syswrite(opts) if opts
28
+ end
29
+
30
+ private
31
+
32
+ def __in_option(mouse, mouse_move, focus)
33
+ opts = +(mouse ? '1000' : '')
34
+ # highlight: '1001'
35
+ # drag: '1002'
36
+ opts << ';1003' if mouse_move
37
+ opts << ';1004' if focus
38
+ # ext: '1005'
39
+ # sgr: '1006'
40
+ # urxvt: '1015'
41
+ # pixel: '1016'
42
+ "\e[?#{opts};1006" unless opts.empty?
43
+ end
44
+ end
45
+
46
+ private_constant :AnsiInput
47
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminal
4
+ module DumpInput
5
+ def input_mode = :dumb
6
+
7
+ def on_key_event(*_)
8
+ raise('already reading key events') if (@in_recursion += 1) != 1
9
+ return unless block_given?
10
+ while (raw = __getc)
11
+ opts = dumb_keys[raw.ord] if raw.size == 1
12
+ return unless yield(KeyEvent.new(raw, *opts))
13
+ end
14
+ rescue IOError, SystemCallError
15
+ __input_error
16
+ false
17
+ ensure
18
+ @in_recursion -= 1
19
+ end
20
+
21
+ private
22
+
23
+ def dumb_keys
24
+ @dumb_keys ||= {
25
+ 0x05 => ['c', 4],
26
+ 0x08 => :Back,
27
+ 0x09 => :Tab,
28
+ 0x0a => :Enter,
29
+ 0x0d => :Return,
30
+ 0x1b => :Esc
31
+ }.compare_by_identity.freeze
32
+ end
33
+
34
+ def __getc
35
+ STDIN.getc
36
+ rescue Interrupt
37
+ "\x05"
38
+ end
39
+ end
40
+
41
+ private_constant :DumpInput
42
+ end
@@ -3,208 +3,117 @@
3
3
  require 'io/console'
4
4
 
5
5
  module Terminal
6
- class << self
7
- # Supported input mode.
8
- #
9
- # @attribute [r] input_mode
10
- #
11
- # @return [:csi_u]
12
- # when [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol)
13
- # supported
14
- # @return [:legacy]
15
- # for standard terminal
16
- # @return [:dumb]
17
- # for non-interactive input (pipes etc.)
18
- # @return [:error]
19
- # when input device is not avail (closed)
6
+ # @!group Attributes
7
+
8
+ # @attribute [r] self.input_mode
9
+ #
10
+ # Supported input mode.
11
+ #
12
+ # @return [:csi_u]
13
+ # when [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol)
14
+ # supported
15
+ # @return [:legacy]
16
+ # for standard terminal
17
+ # @return [:dumb]
18
+ # for non-interactive input (pipes etc.)
19
+ # @return [:error]
20
+ # when input device is not avail (closed)
21
+
22
+ # @!endgroup
23
+
24
+ # @!group Input methods
25
+
26
+ # @!method self.read_key_event
27
+ # Read next {KeyEvent} from standard input.
28
+ #
29
+ # @return [KeyEvent] next event
30
+ # @return [nil] in error case
31
+
32
+ # @!method self.on_key_event(mouse: false, mouse_move: false, focus: false, &block)
33
+ # Event loop for key and mouse events.
34
+ #
35
+ # @param mouse [true, false]
36
+ # whether mouse buttons should be reported
37
+ # @param mouse_move [true, false]
38
+ # whether mouse movement should be reported
39
+ # @param focus [true, false]
40
+ # whether focus/unfocus of terminal window should be reported
41
+ # @yieldparam event [KeyEvent]
42
+ # next event
43
+ # @yieldreturn [true, false]
44
+ # whether the loop should be continued
45
+ # @return [true]
46
+ # when the loop was started
47
+ # @return [false]
48
+ # when the current input device is not available
49
+ # @return [nil]
50
+ # when no block was given
51
+
52
+ # @!endgroup
53
+
54
+ module Input
20
55
  def input_mode
21
- @input_mode ||= find_input_mode
22
- end
23
-
24
- # Read next keyboard input.
25
- #
26
- # The input will be returned as named key codes like "Ctrl+c" by default.
27
- # This can be changed by `mode`.
28
- #
29
- # @deprecated Use rather {read_key_event} for better input handling.
30
- #
31
- # @param mode [:named, :raw, :both] modifies the result
32
- # @return [String] key code ("as is") in `:raw` mode
33
- # @return [String] key name in `:named` mode
34
- # @return [[String, String]] key code and key name in `:both` mode
35
- # @return [nil] in error case
36
- def read_key(mode: :named)
37
- warn(
38
- 'Terminal.read_key is deprecaded; use Terminal.read_key_event instead.',
39
- uplevel: 1
40
- )
41
- event = read_key_event or return
42
- return event.raw if mode == :raw
43
- key, name = event
44
- mode == :both ? [key, name] : name || key
45
- end
46
-
47
- # Read next {KeyEvent} from standard input.
48
- #
49
- # @return [KeyEvent] next event
50
- # @return [nil] in error case
51
- def read_key_event
52
- prevent_input_recursion do
53
- case input_mode
54
- when :dumb
55
- raw = read_dumb or return
56
- opts = @dumb_keys[raw.ord] if raw.size == 1
57
- KeyEvent.new(raw, *opts)
58
- when :csi_u, :legacy
59
- raw = read_tty or return
60
- KeyEvent[raw]
61
- end
56
+ @in_recursion = 0
57
+ case @input_mode = __find_input_mode
58
+ when :legacy, :csi_u
59
+ @in = IO.console
60
+ require_relative 'input/ansi'
61
+ extend AnsiInput
62
+ when :dumb
63
+ require_relative 'input/dumb'
64
+ extend DumpInput
65
+ else
66
+ __input_error
62
67
  end
68
+ @input_mode
63
69
  end
64
70
 
65
- # Event loop for key and mouse events.
66
- #
67
- # @param mouse [true, false]
68
- # whether mouse buttons should be reported
69
- # @param mouse_move [true, false]
70
- # whether mouse movement should be reported
71
- # @param focus [true, false]
72
- # whether focus/unfocus of terminal window should be reported
73
- # @yieldparam event [KeyEvent]
74
- # next event
75
- # @yieldreturn [true, false]
76
- # whether the loop should be continued
77
- # @return [true]
78
- # when the loop was started
79
- # @return [false]
80
- # when the current input device is not available
81
- # @return [nil]
82
- # when no block was given
83
71
  def on_key_event(mouse: false, mouse_move: false, focus: false, &block)
84
- return unless block
85
- prevent_input_recursion do
86
- case input_mode
87
- when :dumb
88
- on_bumb_key_event(&block)
89
- true
90
- when :csi_u, :legacy
91
- on_tty_key_event(mouse_option(mouse, mouse_move, focus), &block)
92
- true
93
- else
94
- false
95
- end
96
- end
97
- end
98
-
99
- private
100
-
101
- def prevent_input_recursion
102
- if (@key_event += 1) != 1
103
- raise(RuntimeError, 'already reading key events', caller(2))
104
- end
105
- begin
106
- yield
107
- ensure
108
- @key_event -= 1
109
- end
72
+ input_mode
73
+ on_key_event(mouse: mouse, mouse_move: mouse_move, focus: focus, &block)
110
74
  end
111
75
 
112
- def mouse_option(mouse, mouse_move, focus)
113
- opts = +(mouse ? '1000' : '')
114
- # highlight: '1001'
115
- # drag: '1002'
116
- opts << ';1003' if mouse_move
117
- opts << ';1004' if focus
118
- # ext: '1005'
119
- # sgr: '1006'
120
- # urxvt: '1015'
121
- # pixel: '1016'
122
- "\e[?#{opts};1006" unless opts.empty?
123
- end
76
+ def read_key_event = on_key_event { return _1 }
124
77
 
125
- def on_tty_key_event(opts)
126
- STDIN.noecho do |stdin|
127
- opts &&= raw_write("#{opts}h") ? "#{opts}l" : nil
128
- while (raw = stdin.getch)
129
- (yield(KeyEvent[raw]) ? next : break) if raw != "\e"
130
- lesci = 0
131
- while String === (nc = stdin.read_nonblock(1, exception: false))
132
- lesci = raw.size if nc == "\e"
133
- raw << nc
134
- end
135
- (yield(KeyEvent[raw]) ? next : break) if lesci < 2
136
- break unless raw[1..].split("\e").all? { yield(KeyEvent["\e#{_1}"]) }
137
- end
138
- end
139
- rescue Interrupt
140
- # nop
141
- rescue IOError, SystemCallError
142
- @input_mode = :error
143
- ensure
144
- raw_write(opts) if opts
145
- end
78
+ private
146
79
 
147
- def on_bumb_key_event
148
- while (raw = read_dumb)
149
- opts = @dumb_keys[raw.ord] if raw.size == 1
150
- return unless yield(KeyEvent.new(raw, *opts))
80
+ def __input_error
81
+ class << self
82
+ alias input_mode input_mode
83
+ def input_mode = :error
84
+ alias read_key_event read_key_event
85
+ def read_key_event = false
86
+ alias on_key_event on_key_event
87
+ def on_key_event(*_) = false
151
88
  end
89
+ @in = nil
152
90
  end
153
91
 
154
- def find_input_mode
92
+ def __find_input_mode
155
93
  im = ENV['INPUT_MODE']
156
94
  return :legacy if im == 'legacy'
157
95
  return :dumb if im == 'dumb' || !STDIN.tty?
158
- STDIN.noecho do |stdin|
159
- return :legacy unless raw_write("\e[>1u\e[?u\e[c")
160
- inp = +''
161
- inp << stdin.getch until inp.rindex('c')
162
- return :legacy unless inp.include?("\e[?1u")
163
- at_exit { raw_write("\e[<u") }
164
- :csi_u
96
+ con = IO.console
97
+ return :legacy if con.syswrite("\e[>1u\e[?u\e[c") != 12
98
+ inp = +''
99
+ inp << con.getch until inp.rindex('c')
100
+ return :legacy unless inp.include?("\e[?1u")
101
+ at_exit do
102
+ IO.console.syswrite("\e[<u")
103
+ rescue StandardError
104
+ # nop
165
105
  end
106
+ :csi_u
166
107
  rescue Interrupt
167
108
  :legacy
168
109
  rescue IOError, SystemCallError
110
+ __input_error
169
111
  :error
170
112
  end
171
-
172
- def read_dumb
173
- STDIN.getc
174
- rescue Interrupt
175
- "\x05"
176
- rescue IOError, SystemCallError
177
- @input_mode = :error
178
- nil
179
- end
180
-
181
- def read_tty
182
- STDIN.noecho do |stdin|
183
- if (key = stdin.getch) == "\e"
184
- while (nc = stdin.read_nonblock(1, exception: false))
185
- String === nc ? key << nc : break
186
- end
187
- end
188
- key
189
- end
190
- rescue Interrupt
191
- nil
192
- rescue IOError, SystemCallError
193
- @input_mode = :error
194
- nil
195
- end
196
113
  end
197
114
 
198
- @dumb_keys = {
199
- 0x05 => ['c', 4],
200
- 0x08 => :Back,
201
- 0x09 => :Tab,
202
- 0x0a => :Enter,
203
- 0x0d => :Return,
204
- 0x1b => :Esc
205
- }.compare_by_identity.freeze
206
-
207
- @key_event = 0
115
+ extend Input
208
116
 
117
+ private_constant :Input
209
118
  autoload :KeyEvent, "#{__dir__}/input/key_event.rb"
210
119
  end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminal
4
+ module AnsiOutput
5
+ def output_mode = :ansi
6
+ def ansi? = true
7
+ def tui? = %i[csi_u legacy].include?(input_mode)
8
+ def colors = (@colors ||= Detect.colors)
9
+
10
+ def columns=(value)
11
+ self.size = [rows, value]
12
+ end
13
+
14
+ def pos
15
+ @con&.cursor
16
+ rescue IOError, SystemCallError
17
+ @con = nil
18
+ end
19
+
20
+ def pos=(pos)
21
+ @con&.cursor = pos
22
+ rescue IOError, SystemCallError
23
+ @con = nil
24
+ end
25
+
26
+ def rows=(value)
27
+ self.size = [value, columns]
28
+ end
29
+
30
+ def size
31
+ @size ||= @con&.winsize || __default_size
32
+ rescue IOError, SystemCallError
33
+ @con = nil
34
+ @size = __default_size
35
+ end
36
+
37
+ def size=(size)
38
+ @con&.winsize = size
39
+ rescue IOError, SystemCallError
40
+ @con = nil
41
+ end
42
+
43
+ def raw_write(str)
44
+ @out.syswrite(str)
45
+ rescue IOError, SystemCallError
46
+ __output_error(nil)
47
+ end
48
+
49
+ def <<(object)
50
+ @out.write(Ansi.bbcode(object)) if object != nil
51
+ self
52
+ rescue IOError, SystemCallError
53
+ __output_error(self)
54
+ end
55
+
56
+ def print(*objects, bbcode: true)
57
+ return if objects.empty?
58
+ return @out.print(*objects) unless bbcode
59
+ @out.print(*objects.map! { Ansi.bbcode(_1) })
60
+ rescue IOError, SystemCallError
61
+ __output_error(nil)
62
+ end
63
+
64
+ def puts(*objects, bbcode: true)
65
+ return @out.puts(objects.empty? ? nil : objects) unless bbcode
66
+ objects.flatten!
67
+ @out.puts(objects.empty? ? nil : objects.map! { Ansi.bbcode(_1) })
68
+ rescue IOError, SystemCallError
69
+ __output_error(nil)
70
+ end
71
+
72
+ def hide_cursor
73
+ raw_write(Ansi::CURSOR_HIDE) if (@cc += 1) == 1
74
+ self
75
+ end
76
+
77
+ def show_cursor
78
+ raw_write(Ansi::CURSOR_SHOW) if @cc > 0 && (@cc -= 1).zero?
79
+ self
80
+ end
81
+
82
+ def show_alt_screen
83
+ raw_write(Ansi::SCREEN_ALTERNATE) if (@as += 1) == 1
84
+ self
85
+ end
86
+
87
+ def hide_alt_screen
88
+ raw_write(Ansi::SCREEN_ALTERNATE_OFF) if @as > 0 && (@as -= 1).zero?
89
+ self
90
+ end
91
+
92
+ private
93
+
94
+ def __output_error(ret)
95
+ require_relative 'dumb'
96
+ extend DumbOutput
97
+ @size = @cc = @as = nil
98
+ __output_error(ret)
99
+ end
100
+
101
+ def __init_tty
102
+ require('io/console')
103
+ @con = IO.console
104
+ Signal.trap('WINCH') { @size = nil } if Signal.list.key?('WINCH')
105
+ end
106
+
107
+ private_class_method def self.extended(mod)
108
+ mod.instance_variable_set(:@cc, 0)
109
+ mod.instance_variable_set(:@as, 0)
110
+ end
111
+ end
112
+
113
+ private_constant :AnsiOutput
114
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminal
4
+ module DumbOutput
5
+ def output_mode = :dumb
6
+ def ansi? = false
7
+ def tui? = false
8
+ def colors = 2
9
+ def pos = nil
10
+ def size = (@size ||= __default_size)
11
+ def hide_cursor = self
12
+ def show_cursor = self
13
+ def show_alt_screen = self
14
+ def hide_alt_screen = self
15
+
16
+ def columns=(_)
17
+ # nop
18
+ end
19
+
20
+ def pos=(_)
21
+ # nop
22
+ end
23
+
24
+ def rows=(_)
25
+ # nop
26
+ end
27
+
28
+ def size=(_)
29
+ # nop
30
+ end
31
+
32
+ def raw_write(_) = nil
33
+
34
+ def <<(object)
35
+ @out.write(Ansi.plain(object)) if object != nil
36
+ self
37
+ rescue IOError, SystemCallError
38
+ __output_error(self)
39
+ end
40
+
41
+ def print(*objects, bbcode: true)
42
+ return if objects.empty?
43
+ @out.print(*objects.map!(&(bbcode ? @plain : @undecorate)))
44
+ rescue IOError, SystemCallError
45
+ __output_error(nil)
46
+ end
47
+
48
+ def puts(*objects, bbcode: true)
49
+ objects.flatten!
50
+ return @out.puts if objects.empty?
51
+ @out.puts(*objects.map!(&(bbcode ? @plain : @undecorate)))
52
+ rescue IOError, SystemCallError
53
+ __output_error(nil)
54
+ end
55
+
56
+ private
57
+
58
+ def __output_error(ret)
59
+ class << self
60
+ def output_mode = :error
61
+ def <<(_) = self
62
+ def print(*_) = nil
63
+ def puts(*_) = nil
64
+ end
65
+ @out = @plain = @undecorate = nil
66
+ ret
67
+ end
68
+
69
+ private_class_method def self.extended(mod)
70
+ mod.instance_variable_set(:@plain, ->(s) { Ansi.plain(s) })
71
+ mod.instance_variable_set(:@undecorate, ->(s) { Ansi.undecorate(s) })
72
+ end
73
+ end
74
+
75
+ private_constant :DumbOutput
76
+ end