terminal_rb 0.17.1 → 0.18.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: 3ccb3312146fca100bd13e4c079e5cf6f4a33e73ef9eb526765df1309ae5168e
4
- data.tar.gz: ce4f5679183067ac7f7d84196db0ad9a06a22878fda79ec302fbacc8047699fa
3
+ metadata.gz: a718915d07704084ceeb6a395a4a40d97feb52c17048f8024b3493afb0d8a1c5
4
+ data.tar.gz: 252bb2965c7b5d33e58fb0752e1bb171cbe259de315494f5e547803554c843a7
5
5
  SHA512:
6
- metadata.gz: 8a898bce2dc22f5b1d873ace6cef5ea53046318d68bc80062898ec7be43553acde5a7709c1b1043666533e91fbecec3e66dd0372c0adb1642803b4273f0c91d0
7
- data.tar.gz: d9a06bd5ef76f97a0331bb2e05681fae5103dbe69a21c8e5ac0b97cb7342a6569d0e595b90b00744f22ed9253fc858fdbcd8e81e37ce595bfd1bb830c5cea2b9
6
+ metadata.gz: 9d006c4e463e3522b3e3c63bd343d2806cf4dd11bdaa6e9cf82b73ee7fe84d40296b9d919d3b0ea7664edca354e28e99302e0e01b87f04e5fa0dd5ccecd4a6b2
7
+ data.tar.gz: bb3a2b01dbd455bd66c2dbddcbd29dfca325d134b40b34b1de14168cb8c0c20be001f0bd20e78cd64f18a56bde7fe17a73673e7d7c249cb446cafeaef1c27f80
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/index)
7
+ - Help: [rubydoc.info](https://rubydoc.info/gems/terminal_rb/0.18.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
 
@@ -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
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminal
4
+ class << self
5
+ # @!group Attributes
6
+
7
+ # @attribute [r] tui?
8
+ #
9
+ # Return `true` if the current terminal supports ANSI control codes for
10
+ # input _and_ output.
11
+ # In this case not only all output methods ({<<}, {print}, {puts}) will
12
+ # forward ANSI control codes to the terminal and translate BBCode
13
+ # (see {Ansi.bbcode}). But also the input methods {read_key_event} and
14
+ # {on_key_event} will support extended key codes, mouse and focus events.
15
+ #
16
+ # @see output_mode
17
+ # @see ansi?
18
+ #
19
+ # @return [true, false]
20
+ # whether ANSI control codes are supported for input and output
21
+
22
+ # @attribute [r] output_mode
23
+ #
24
+ # Supported output mode.
25
+ #
26
+ # @return [:ansi]
27
+ # All output methods ({<<}, {print}, {puts}) will forward ANSI control
28
+ # codes to the terminal and translate BBCode (see {Ansi.bbcode}).
29
+ # @return [:dumb]
30
+ # All output methods will not forward ANSI control codes and BBCodes will
31
+ # be removed.
32
+ # The {colors} method will just return 2 (two).
33
+ # @return [:error]
34
+ # When the output device signaled an error or was closed, nothing will
35
+ # be written to the terminal.
36
+
37
+ # @!endgroup
38
+
39
+ # @!group Output attributes
40
+
41
+ # @attribute [r] ansi?
42
+ #
43
+ # @see output_mode
44
+ # @see tui?
45
+ #
46
+ # @return [true, false]
47
+ # whether ANSI control codes are supported for output
48
+
49
+ # @attribute [r] colors
50
+ #
51
+ # Number of supported colors.
52
+ # The detection checks various conditions to find the correct value. The
53
+ # most common values are
54
+ #
55
+ # - `16_777_216` for 24-bit encoding ({true_color?} return true)
56
+ # - `256` for 8-Bit encoding
57
+ # - `52` for some Unix terminals with an extended color palette
58
+ # - `8` for 3-/4-bit encoding
59
+ # - `2` if ANSI is not supported in general ({ansi?} return false)
60
+ #
61
+ # @return [Integer]
62
+ # number of supported colors
63
+
64
+ # @attribute [r] true_color?
65
+ #
66
+ # @see colors
67
+ #
68
+ # @return [true, false]
69
+ # whether true colors are supported
70
+ def true_color? = (colors == 16_777_216)
71
+
72
+ # @attribute [r] columns
73
+ #
74
+ # Screen column count.
75
+ # See {size} for support and detection details.
76
+ #
77
+ # @return [Integer]
78
+ # number of available columns
79
+ def columns = size[1]
80
+ #
81
+ # @attribute [w] columns
82
+
83
+ # @attribute [r] rows
84
+ #
85
+ # Screen row count.
86
+ # See {size} for support and detection details.
87
+ #
88
+ # @return [Integer]
89
+ # number of available rows
90
+ def rows = size[0]
91
+ #
92
+ # @attribute [w] rows
93
+
94
+ # @attribute [r] pos
95
+ #
96
+ # Current cursor position.
97
+ # This is only available when ANSI is supported ({ansi?} return true).
98
+ #
99
+ # @return [[Integer, Integer]]
100
+ # cursor position as rows and columns
101
+ # @return [nil]
102
+ # for incompatible terminals
103
+ #
104
+ # @attribute [w] pos
105
+
106
+ # @attribute [r] size
107
+ #
108
+ # Screen size as a tuple of {rows} and {columns}.
109
+ #
110
+ # If the terminal does not support the report of it's dimension or ANSI
111
+ # is not supported in general then environment variables `COLUMNS` and
112
+ # `LINES` will be used.
113
+ # If this failed `[25, 80]` will be returned as default.
114
+ #
115
+ # Setting the terminal size is not widely supported.
116
+ #
117
+ # @see rows
118
+ # @see columns
119
+ #
120
+ # @return [[Integer, Integer]]
121
+ # available screen size as rows and columns
122
+ #
123
+ # @attribute [w] size
124
+
125
+ # @!endgroup
126
+
127
+ # @!group Output methods
128
+
129
+ # @!method <<(object)
130
+ #
131
+ # Writes the given object to the terminal.
132
+ # Interprets embedded BBCode (see {Ansi.bbcode}).
133
+ #
134
+ # @param object [#to_s] object to write
135
+ # @return [Terminal] itself
136
+
137
+ # @!method print(*objects, bbcode: true)
138
+ #
139
+ # Writes the given objects to the terminal.
140
+ # Optionally interprets embedded BBCode (see {Ansi.bbcode}).
141
+ #
142
+ # @param objects [Array<#to_s>] any number of objects to write
143
+ # @param bbcode [true|false] whether to interpret embedded BBCode
144
+ # @return [nil]
145
+
146
+ # @!method puts(*objects, bbcode: true)
147
+ #
148
+ # Writes the given objects to the terminal.
149
+ # Writes a newline after each object that does not already end with a
150
+ # newline sequence in it's String represenation.
151
+ # If called without any arguments, writes a newline only.
152
+ #
153
+ # Optionally interprets embedded BBCode (see {Ansi.bbcode}).
154
+ #
155
+ # @param objects [Array<#to_s>] any number of objects to write
156
+ # @param bbcode [true|false] whether to interpret embedded BBCode
157
+ # @return [nil]
158
+
159
+ # @!endgroup
160
+
161
+ # @!group Output helper methods
162
+
163
+ # @!method hide_cursor
164
+ #
165
+ # Hide the cursor.
166
+ # Will not send the control code if the cursor is already hidden.
167
+ #
168
+ # When you called {hide_cursor} n-times you need to call {show_cursor}
169
+ # n-times to show the cursor again.
170
+ #
171
+ # @return [Terminal] itself
172
+
173
+ # @!method show_cursor
174
+ #
175
+ # Show the cursor.
176
+ # Will not send the control code if the cursor is not hidden.
177
+ #
178
+ # When you called {hide_cursor} n-times you need to call {show_cursor}
179
+ # n-times to show the cursor again.
180
+ #
181
+ # @return [Terminal] itself
182
+
183
+ # @!method show_alt_screen
184
+ #
185
+ # Show the alternate screen.
186
+ # Will not send the control code if the alternate screen is already used.
187
+ #
188
+ # When you called {show_alt_screen} n-times you need to call
189
+ # {hide_alt_screen} n-times to show the default screen again.
190
+ #
191
+ # @return [Terminal] itself
192
+
193
+ # @!method hide_alt_screen
194
+ #
195
+ # Hide the alternate screen.
196
+ # Will not send the control code if the alternate screen is not used.
197
+ #
198
+ # When you called {show_alt_screen} n-times you need to call
199
+ # {hide_alt_screen} n-times to show the default screen again.
200
+ #
201
+ # @return [Terminal] itself
202
+
203
+ # @!endgroup
204
+
205
+ private
206
+
207
+ def __default_size
208
+ rows = ENV['LINES'].to_i
209
+ columns = ENV['COLUMNS'].to_i
210
+ [rows > 0 ? rows : 25, columns > 0 ? columns : 80]
211
+ end
212
+
213
+ def __output_modes
214
+ return false if ENV.key?('NO_COLOR') || ENV['TERM'] == 'dumb'
215
+ [tty = STDOUT.tty?, ENV['ANSI'] == 'force' || tty]
216
+ rescue IOError, SystemCallError
217
+ nil
218
+ end
219
+ end
220
+
221
+ tty, ansi = __output_modes
222
+ if tty.nil?
223
+ require_relative 'output/dumb'
224
+ extend DumbOutput
225
+ __output_error(nil)
226
+ else
227
+ @out = STDOUT
228
+ @out.sync = true if defined?(@out.sync)
229
+ if ansi
230
+ require_relative 'output/ansi'
231
+ extend AnsiOutput
232
+ __init_tty if tty
233
+ else
234
+ require_relative 'output/dumb'
235
+ extend DumbOutput
236
+ end
237
+ end
238
+ end
@@ -53,9 +53,13 @@ RSpec.shared_context 'with Terminal.rb' do |ansi: true, application: :kitty, col
53
53
  nil
54
54
  end
55
55
 
56
- allow(Terminal).to receive(:raw_write) do |object|
57
- stdout.push(object = object.to_s)
58
- object.bytesize
56
+ if ansi
57
+ allow(Terminal).to receive(:raw_write) do |object|
58
+ stdout.push(object = object.to_s)
59
+ object.bytesize
60
+ end
61
+ else
62
+ allow(Terminal).to receive(:raw_write).and_return(nil)
59
63
  end
60
64
 
61
65
  allow(Terminal).to receive(:read_key_event) do
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Terminal
4
4
  # The version number of the gem.
5
- VERSION = '0.17.1'
5
+ VERSION = '0.18.0'
6
6
  end
data/lib/terminal.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'terminal/ansi'
3
+ require_relative 'terminal/output'
4
4
  require_relative 'terminal/input'
5
+ require_relative 'terminal/ansi'
5
6
 
6
7
  #
7
8
  # Terminal access with support for ANSI control codes and
@@ -16,41 +17,7 @@ require_relative 'terminal/input'
16
17
  #
17
18
  module Terminal
18
19
  class << self
19
- # Return `true` if the current terminal supports ANSI control codes for
20
- # output.
21
- # In this case all output methods ({<<}, {print}, {puts}) will forward ANSI
22
- # control codes to the terminal and translate BBCode (see {Ansi.bbcode}).
23
- #
24
- # When `false` is returned (ANSI is not supported) the output methods will
25
- # not forward ANSI control codes and BBCodes will be removed.
26
- # The {colors} method will just return 2 (two).
27
- #
28
- # @see tui?
29
- #
30
- # @attribute [r] ansi?
31
- # @return [true, false] whether ANSI control codes are supported for output
32
- def ansi? = @ansi
33
-
34
- # Return `true` if the current terminal supports ANSI control codes for
35
- # input _and_ output.
36
- # In this case not only all output methods ({<<}, {print}, {puts}) will
37
- # forward ANSI control codes to the terminal and translate BBCode
38
- # (see {Ansi.bbcode}). But also the input methods {read_key_event} and
39
- # {on_key_event} will support extended key codes, mouse and focus events.
40
- #
41
- # @see ansi?
42
- #
43
- # @attribute [r] tui?
44
- # @return [true, false]
45
- # whether ANSI control codes are supported for input and output
46
- def tui?
47
- case input_mode
48
- when :csi_u, :legacy
49
- @ansi
50
- else
51
- false
52
- end
53
- end
20
+ # @!group Attributes
54
21
 
55
22
  # Terminal application identifier.
56
23
  #
@@ -66,190 +33,9 @@ module Terminal
66
33
  # @return [Symbol, nil] the application identifier
67
34
  def application = (@application ||= Detect.application)
68
35
 
69
- # Number of supported colors.
70
- # The detection checks various conditions to find the correct value. The
71
- # most common values are
72
- #
73
- # - `16_777_216` for 24-bit encoding ({true_color?} return true)
74
- # - `256` for 8-Bit encoding
75
- # - `52` for some Unix terminals with an extended color palette
76
- # - `8` for 3-/4-bit encoding
77
- # - `2` if ANSI is not supported in general ({ansi?} return false)
78
- #
79
- # @attribute [r] colors
80
- # @return [Integer] number of supported colors
81
- def colors = (@colors ||= ansi? ? Detect.colors : 2)
82
-
83
- # Screen column count.
84
- # See {size} for support and detection details.
85
- #
86
- # @attribute [r] columns
87
- # @return [Integer] number of available columns
88
- def columns = size[1]
89
-
90
- # @attribute [w] columns
91
- def columns=(value)
92
- self.size = [rows, value]
93
- end
94
-
95
- # Current cursor position.
96
- # This is only available when ANSI is supported ({ansi?} return true).
97
- #
98
- # @attribute [r] pos
99
- # @return [[Integer, Integer]] cursor position as rows and columns
100
- # @return [nil] for incompatible terminals
101
- def pos
102
- @con&.cursor
103
- rescue IOError
104
- @con = nil
105
- end
106
-
107
- # @attribute [w] pos
108
- def pos=(pos)
109
- @con&.cursor = pos
110
- rescue IOError
111
- @con = nil
112
- end
113
-
114
- # Screen row count.
115
- # See {size} for support and detection details.
116
- #
117
- # @attribute [r] rows
118
- # @return [Integer] number of available rows
119
- def rows = size[0]
120
-
121
- # @attribute [w] rows
122
- def rows=(value)
123
- self.size = [value, columns]
124
- end
125
-
126
- # Screen size as a tuple of {rows} and {columns}.
127
- #
128
- # If the terminal does not support the report of it's dimension or ANSI
129
- # is not supported in general then environment variables `COLUMNS` and
130
- # `LINES` will be used.
131
- # If this failed `[25, 80]` will be returned as default.
132
- #
133
- # Setting the terminal size is not widely supported.
134
- #
135
- # @see rows
136
- # @see columns
137
- #
138
- # @attribute [r] size
139
- # @return [[Integer, Integer]] available screen size as rows and columns
140
- def size
141
- @size ||= @inf&.winsize || _default_size
142
- rescue IOError
143
- @inf = nil
144
- @size = _default_size
145
- end
146
-
147
- # @attribute [w] size
148
- def size=(size)
149
- @inf&.winsize = size
150
- rescue IOError
151
- @inf = nil
152
- end
153
-
154
- # @see colors
155
- # @attribute [r] true_color?
156
- # @return [true, false] whether true colors are supported
157
- def true_color? = (colors == 16_777_216)
158
-
159
- # Hide the cursor.
160
- # Will not send the control code if the cursor is already hidden.
161
- #
162
- # When you called {hide_cursor} n-times you need to call {show_cursor}
163
- # n-times to show the cursor again.
164
- #
165
- # @return [Terminal] itself
166
- def hide_cursor
167
- raw_write(Ansi::CURSOR_HIDE) if ansi? && (@cc += 1) == 1
168
- self
169
- end
170
-
171
- # Show the cursor.
172
- # Will not send the control code if the cursor is not hidden.
173
- #
174
- # When you called {hide_cursor} n-times you need to call {show_cursor}
175
- # n-times to show the cursor again.
176
- #
177
- # @return [Terminal] itself
178
- def show_cursor
179
- raw_write(Ansi::CURSOR_SHOW) if @cc > 0 && (@cc -= 1).zero?
180
- self
181
- end
36
+ # @!endgroup
182
37
 
183
- # Show the alternate screen.
184
- # Will not send the control code if the alternate screen is already used.
185
- #
186
- # When you called {show_alt_screen} n-times you need to call
187
- # {hide_alt_screen} n-times to show the default screen again.
188
- #
189
- # @return [Terminal] itself
190
- def show_alt_screen
191
- raw_write(Ansi::SCREEN_ALTERNATE) if ansi? && (@as += 1) == 1
192
- self
193
- end
194
-
195
- # Hide the alternate screen.
196
- # Will not send the control code if the alternate screen is not used.
197
- #
198
- # When you called {show_alt_screen} n-times you need to call
199
- # {hide_alt_screen} n-times to show the default screen again.
200
- #
201
- # @return [Terminal] itself
202
- def hide_alt_screen
203
- raw_write(Ansi::SCREEN_ALTERNATE_OFF) if @as > 0 && (@as -= 1).zero?
204
- self
205
- end
206
-
207
- # Writes the given object to the terminal.
208
- # Interprets embedded BBCode.
209
- #
210
- # @param object [#to_s] object to write
211
- # @return [Terminal] itself
212
- def <<(object)
213
- @out.write(Ansi.bbcode(object)) if @out && object != nil
214
- self
215
- rescue IOError
216
- @out = nil
217
- self
218
- end
219
-
220
- # Writes the given objects to the terminal.
221
- # Optionally interprets embedded BBCode.
222
- #
223
- # @param objects [Array<#to_s>] any number of objects to write
224
- # @param bbcode [true|false] whether to interpret embedded BBCode
225
- # @return [nil]
226
- def print(*objects, bbcode: true)
227
- return unless @out
228
- return @out.print(*objects) unless bbcode
229
- objects.each { @out.write(Ansi.bbcode(_1)) }
230
- nil
231
- rescue IOError
232
- @out = nil
233
- end
234
-
235
- # Writes the given objects to the terminal.
236
- # Writes a newline after each object that does not already end with a
237
- # newline sequence in it's String represenation.
238
- # If called without any arguments, writes a newline only.
239
- #
240
- # Optionally interprets embedded BBCode.
241
- #
242
- # @param (see print)
243
- # @return (see print)
244
- def puts(*objects, bbcode: true)
245
- return unless @out
246
- return @out.puts if objects.empty?
247
- return @out.puts(objects) unless bbcode
248
- objects.flatten!
249
- objects.empty? ? @out.puts : @out.puts(objects.map! { Ansi.bbcode(_1) })
250
- rescue IOError
251
- @out = nil
252
- end
38
+ # @!group Tool methods
253
39
 
254
40
  # Execute a command and report command output line by line
255
41
  # or capture the output.
@@ -316,77 +102,7 @@ module Terminal
316
102
  ]
317
103
  end
318
104
 
319
- # @private
320
- def raw_write(str)
321
- @out&.syswrite(str)
322
- rescue IOError
323
- @out = nil
324
- end
325
-
326
- private
327
-
328
- def _default_size
329
- rows = ENV['LINES'].to_i
330
- columns = ENV['COLUMNS'].to_i
331
- [rows > 0 ? rows : 25, columns > 0 ? columns : 80]
332
- end
333
-
334
- def _determine_modes
335
- # order is important!
336
- tty = STDOUT.tty?
337
- return tty, true if ENV['ANSI'] == 'force'
338
- return false, false if ENV.key?('NO_COLOR') || ENV['TERM'] == 'dumb'
339
- [tty, tty]
340
- rescue IOError
341
- [nil, false]
342
- end
343
- end
344
-
345
- @cc = @as = 0
346
- tty, @ansi = _determine_modes
347
-
348
- unless tty.nil?
349
- @out = STDOUT
350
- @out.sync = true
351
- if tty
352
- @inf = STDOUT
353
- @con = IO.console
354
- Signal.trap('WINCH') { @size = nil } if Signal.list.key?('WINCH')
355
- end
356
- end
357
-
358
- unless @ansi
359
- @plain = ->(s) { Ansi.plain(s) }
360
- @undecorate = ->(s) { Ansi.undecorate(s) }
361
-
362
- class << self
363
- alias << <<
364
- def <<(object)
365
- @out.write(Ansi.plain(object)) if @out && object != nil
366
- self
367
- rescue IOError
368
- @out = nil
369
- self
370
- end
371
-
372
- alias print print
373
- def print(*objects, bbcode: true)
374
- return if @out.nil? || objects.empty?
375
- @out.print(*objects.map!(&(bbcode ? @plain : @undecorate)))
376
- rescue IOError
377
- @out = nil
378
- end
379
-
380
- alias puts puts
381
- def puts(*objects, bbcode: true)
382
- return unless @out
383
- objects.flatten!
384
- return @out.puts if objects.empty?
385
- @out.puts(objects.map!(&(bbcode ? @plain : @undecorate)))
386
- rescue IOError
387
- @out = nil
388
- end
389
- end
105
+ # @!endgroup
390
106
  end
391
107
 
392
108
  dir = "#{__dir__}/terminal"
data/terminal_rb.gemspec CHANGED
@@ -21,7 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.homepage = 'https://codeberg.org/mblumtritt/Terminal.rb'
22
22
  spec.metadata['source_code_uri'] = spec.homepage
23
23
  spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
24
- spec.metadata['documentation_uri'] = 'https://rubydoc.info/gems/terminal_rb'
24
+ spec.metadata['documentation_uri'] = "https://rubydoc.info/gems/terminal_rb/#{
25
+ Terminal::VERSION
26
+ }"
25
27
  spec.metadata['rubygems_mfa_required'] = 'true'
26
28
  spec.metadata['yard.run'] = 'yard'
27
29
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terminal_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
@@ -35,7 +35,12 @@ files:
35
35
  - lib/terminal/ansi/named_colors.rb
36
36
  - lib/terminal/detect.rb
37
37
  - lib/terminal/input.rb
38
+ - lib/terminal/input/ansi.rb
39
+ - lib/terminal/input/dumb.rb
38
40
  - lib/terminal/input/key_event.rb
41
+ - lib/terminal/output.rb
42
+ - lib/terminal/output/ansi.rb
43
+ - lib/terminal/output/dumb.rb
39
44
  - lib/terminal/rspec/helper.rb
40
45
  - lib/terminal/shell.rb
41
46
  - lib/terminal/text.rb
@@ -50,7 +55,7 @@ licenses:
50
55
  metadata:
51
56
  source_code_uri: https://codeberg.org/mblumtritt/Terminal.rb
52
57
  bug_tracker_uri: https://codeberg.org/mblumtritt/Terminal.rb/issues
53
- documentation_uri: https://rubydoc.info/gems/terminal_rb
58
+ documentation_uri: https://rubydoc.info/gems/terminal_rb/0.18.0
54
59
  rubygems_mfa_required: 'true'
55
60
  yard.run: yard
56
61
  rdoc_options: []
@@ -67,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
72
  - !ruby/object:Gem::Version
68
73
  version: '0'
69
74
  requirements: []
70
- rubygems_version: 3.7.2
75
+ rubygems_version: 4.0.3
71
76
  specification_version: 4
72
77
  summary: Fast terminal access with ANSI, CSIu, mouse events, BBCode, word-wise line
73
78
  break support and much more.