reflex-terminal 0.1.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 +7 -0
- data/.doc/ext/reflex-terminal/native.cpp +19 -0
- data/.doc/ext/reflex-terminal/renderer.cpp +110 -0
- data/.doc/ext/reflex-terminal/terminal.cpp +530 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
- data/.github/workflows/release-gem.yml +60 -0
- data/.github/workflows/tag.yml +35 -0
- data/.github/workflows/test.yml +46 -0
- data/.github/workflows/utils.rb +127 -0
- data/CONTRIBUTING.md +7 -0
- data/ChangeLog.md +6 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/examples/terminal.rb +101 -0
- data/ext/reflex-terminal/defs.h +17 -0
- data/ext/reflex-terminal/extconf.rb +29 -0
- data/ext/reflex-terminal/native.cpp +19 -0
- data/ext/reflex-terminal/renderer.cpp +118 -0
- data/ext/reflex-terminal/terminal.cpp +572 -0
- data/include/reflex/terminal.h +233 -0
- data/include/reflex-terminal/defs.h +36 -0
- data/include/reflex-terminal/renderer.h +57 -0
- data/include/reflex-terminal/ruby/renderer.h +41 -0
- data/include/reflex-terminal/ruby/terminal.h +43 -0
- data/include/reflex-terminal/ruby.h +11 -0
- data/include/reflex-terminal.h +13 -0
- data/lib/reflex/bell_event.rb +15 -0
- data/lib/reflex/terminal.rb +168 -0
- data/lib/reflex/terminal_view.rb +268 -0
- data/lib/reflex-terminal/ext.rb +2 -0
- data/lib/reflex-terminal/extension.rb +41 -0
- data/lib/reflex-terminal.rb +7 -0
- data/reflex-terminal.gemspec +39 -0
- data/src/pty.cpp +236 -0
- data/src/renderer.cpp +365 -0
- data/src/terminal.cpp +1547 -0
- data/src/terminal.h +66 -0
- data/src/win32/pty.cpp +387 -0
- data/test/helper.rb +11 -0
- data/test/test_renderer.rb +113 -0
- data/test/test_terminal.rb +683 -0
- data/test/test_terminal_view.rb +182 -0
- metadata +152 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
require 'xot/util'
|
|
2
|
+
require 'reflex/view'
|
|
3
|
+
require 'reflex/font'
|
|
4
|
+
require 'reflex/color'
|
|
5
|
+
require 'reflex/bell_event'
|
|
6
|
+
require 'reflex/terminal'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module Reflex
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TerminalView < View
|
|
13
|
+
|
|
14
|
+
DEFAULT_FONT_SIZE = 14
|
|
15
|
+
|
|
16
|
+
DEFAULT_FONT_NAME =
|
|
17
|
+
if Xot.osx? then 'Menlo'
|
|
18
|
+
elsif Xot.win32? then 'Consolas'
|
|
19
|
+
else 'DejaVu Sans Mono'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
CURSOR_BLINK_INTERVAL = 0.5
|
|
23
|
+
|
|
24
|
+
def initialize(
|
|
25
|
+
*args,
|
|
26
|
+
terminal: nil, # attach this instead of spawning one
|
|
27
|
+
command: nil, # passed to Terminal#spawn ($SHELL if nil)
|
|
28
|
+
envs: {}, # child's env, a nil value removes one
|
|
29
|
+
font: nil, # a Font, a font name, or [name, size]
|
|
30
|
+
font_size: DEFAULT_FONT_SIZE,
|
|
31
|
+
**kwargs, &block)
|
|
32
|
+
|
|
33
|
+
super(*args, **kwargs, &block)
|
|
34
|
+
@terminal, @command, @envs = terminal, command, envs
|
|
35
|
+
@renderer = ReflexTerminal::Renderer.new
|
|
36
|
+
@cursor_blink = true
|
|
37
|
+
@prev_bells = terminal&.bells || 0
|
|
38
|
+
@font_size = font_size
|
|
39
|
+
self.font = font || DEFAULT_FONT_NAME
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :terminal
|
|
43
|
+
|
|
44
|
+
def font=(font)
|
|
45
|
+
unless font.is_a? Font
|
|
46
|
+
name, size = [font].flatten
|
|
47
|
+
font = Font.new name, size || @font_size
|
|
48
|
+
end
|
|
49
|
+
@renderer.font = font
|
|
50
|
+
@font_size = font.size
|
|
51
|
+
resize_terminal
|
|
52
|
+
redraw
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def font()
|
|
56
|
+
@renderer.font
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def font_size=(size)
|
|
60
|
+
self.font = [font.name, size]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def font_size()
|
|
64
|
+
font.size
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def on_attach(e)
|
|
68
|
+
unless @terminal
|
|
69
|
+
@terminal = Terminal.new
|
|
70
|
+
@terminal.spawn(@envs, *[@command].compact)
|
|
71
|
+
end
|
|
72
|
+
resize_terminal
|
|
73
|
+
focus
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def on_detach(e)
|
|
77
|
+
stop_cursor_blink
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def on_activate(e)
|
|
81
|
+
update_cursor_blink
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def on_deactivate(e)
|
|
85
|
+
update_cursor_blink
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def on_focus(e)
|
|
89
|
+
update_cursor_blink
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def on_update(e)
|
|
93
|
+
t = @terminal || return
|
|
94
|
+
if t.update || @renderer.glyph_count == 0
|
|
95
|
+
@renderer.bake_glyphs t
|
|
96
|
+
redraw
|
|
97
|
+
end
|
|
98
|
+
if t.bells > @prev_bells
|
|
99
|
+
bells, @prev_bells = t.bells - @prev_bells, t.bells
|
|
100
|
+
on_bell BellEvent.new(bells)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def on_draw(e)
|
|
105
|
+
t = @terminal || return
|
|
106
|
+
|
|
107
|
+
@renderer.draw e.painter, t, e.bounds
|
|
108
|
+
draw_cursor e.painter, t
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def on_key_down(e)
|
|
112
|
+
t = @terminal || return
|
|
113
|
+
|
|
114
|
+
# typing changes what a selection covers, so it is dropped. A modifier
|
|
115
|
+
# arrives here on its own, and one held with command is a shortcut for
|
|
116
|
+
# the application rather than something typed
|
|
117
|
+
typed = e.chars && (e.modifiers & %i[command win]).empty?
|
|
118
|
+
t.deselect if typed && t.selection?
|
|
119
|
+
t.scroll_to 0 if t.scroll != 0
|
|
120
|
+
t.write_key e
|
|
121
|
+
start_cursor_blink
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def on_key_up(e)
|
|
125
|
+
@terminal&.write_key e
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def on_pointer_down(e)
|
|
129
|
+
focus
|
|
130
|
+
t = @terminal || return
|
|
131
|
+
|
|
132
|
+
# the child process gets the mouse once it asks for it, unless shift
|
|
133
|
+
# says this drag belongs to the user: the way a terminal lets text be
|
|
134
|
+
# selected inside an application that tracks the mouse itself
|
|
135
|
+
@selecting = e.left? && (!t.mouse_tracking? || e.modifiers.include?(:shift))
|
|
136
|
+
return t.write_pointer(e) unless @selecting
|
|
137
|
+
|
|
138
|
+
x, y = to_cell e.x, e.y
|
|
139
|
+
case e.click_count
|
|
140
|
+
when 1 then t.deselect
|
|
141
|
+
when 2 then t.select_word x, y
|
|
142
|
+
else t.select_line y
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def on_pointer_up(e)
|
|
147
|
+
t = @terminal || return
|
|
148
|
+
t.write_pointer e unless @selecting
|
|
149
|
+
@selecting = false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def on_pointer_move(e)
|
|
153
|
+
t = @terminal || return
|
|
154
|
+
return t.write_pointer(e) unless @selecting
|
|
155
|
+
return unless e.drag? && e.click_count == 1
|
|
156
|
+
|
|
157
|
+
down = e.down || return
|
|
158
|
+
from, to = to_cell(down.x, down.y), to_cell(e.x, e.y)
|
|
159
|
+
if from == to
|
|
160
|
+
t.deselect if t.selection?
|
|
161
|
+
else
|
|
162
|
+
t.select(*from, *to)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def on_wheel(e)
|
|
167
|
+
t = @terminal || return
|
|
168
|
+
|
|
169
|
+
if t.mouse_tracking?
|
|
170
|
+
t.write_wheel e
|
|
171
|
+
else
|
|
172
|
+
rows = (e.dy / @renderer.cell_height).round
|
|
173
|
+
return if rows == 0
|
|
174
|
+
t.scroll_by rows
|
|
175
|
+
redraw
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def on_resize(e)
|
|
180
|
+
resize_terminal
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def on_bell(e)
|
|
184
|
+
# Called with a BellEvent when the terminal received one or more BEL
|
|
185
|
+
# characters (0x07). What a bell means is left to the application: a
|
|
186
|
+
# sound, a flash of the window, a badge, or nothing at all.
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def blinking?()
|
|
192
|
+
window&.active? && focus?
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def update_cursor_blink()
|
|
196
|
+
if blinking?
|
|
197
|
+
start_cursor_blink
|
|
198
|
+
else
|
|
199
|
+
stop_cursor_blink
|
|
200
|
+
redraw
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def start_cursor_blink()
|
|
205
|
+
@cursor_blink = true
|
|
206
|
+
stop_cursor_blink
|
|
207
|
+
@cursor_blinker = interval CURSOR_BLINK_INTERVAL do
|
|
208
|
+
@cursor_blink = !@cursor_blink
|
|
209
|
+
redraw
|
|
210
|
+
end
|
|
211
|
+
redraw
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def stop_cursor_blink()
|
|
215
|
+
@cursor_blinker&.stop
|
|
216
|
+
@cursor_blinker = nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def draw_cursor(painter, terminal)
|
|
220
|
+
x, y, style, visible = terminal.cursor
|
|
221
|
+
return unless visible
|
|
222
|
+
|
|
223
|
+
# ghostty never reports BLOCK_HOLLOW itself -- there is no DECSCUSR for
|
|
224
|
+
# it -- so showing one while the terminal is not taking input is the
|
|
225
|
+
# view's own decision, and it does not blink
|
|
226
|
+
hollow = !blinking?
|
|
227
|
+
return unless hollow || @cursor_blink
|
|
228
|
+
|
|
229
|
+
cw, ch = @renderer.cell_width, @renderer.cell_height
|
|
230
|
+
color = terminal.cursor_color || terminal.foreground_color || Color.new(1, 1, 1)
|
|
231
|
+
|
|
232
|
+
painter.push fill: color, stroke: nil do |p|
|
|
233
|
+
case hollow ? Terminal::CURSOR_BLOCK_HOLLOW : style
|
|
234
|
+
when Terminal::CURSOR_BAR then p.rect x * cw, y * ch, 2, ch
|
|
235
|
+
when Terminal::CURSOR_UNDERLINE then p.rect x * cw, (y + 1) * ch - 2, cw, 2
|
|
236
|
+
when Terminal::CURSOR_BLOCK_HOLLOW
|
|
237
|
+
p.fill nil
|
|
238
|
+
p.stroke color
|
|
239
|
+
p.rect x * cw + 0.5, y * ch + 0.5, cw - 1, ch - 1
|
|
240
|
+
else
|
|
241
|
+
p.fill color.dup.tap {|c| c.alpha = 0.5}
|
|
242
|
+
p.rect x * cw, y * ch, cw, ch
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def to_cell(x, y)
|
|
248
|
+
# Clamped to the screen so that a drag leaving the view keeps selecting
|
|
249
|
+
# up to the edge, rather than asking for a cell the terminal will refuse.
|
|
250
|
+
cw, ch = @renderer.cell_width, @renderer.cell_height
|
|
251
|
+
[(x / cw).floor.clamp(0, @terminal.columns - 1),
|
|
252
|
+
(y / ch).floor.clamp(0, @terminal.rows - 1)]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def resize_terminal()
|
|
256
|
+
return unless @terminal && width > 0 && height > 0
|
|
257
|
+
cw, ch = @renderer.cell_width, @renderer.cell_height
|
|
258
|
+
@terminal.resize(
|
|
259
|
+
(width / cw).floor.clamp(1..),
|
|
260
|
+
(height / ch).floor.clamp(1..),
|
|
261
|
+
cell_width: cw.round, cell_height: ch,
|
|
262
|
+
screen_width: width.to_i, screen_height: height.to_i)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
end# TerminalView
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
end# Reflex
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module ReflexTerminal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module Extension
|
|
5
|
+
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def name(downcase = false)
|
|
9
|
+
super().split('::')[..-2].join.then {|s|
|
|
10
|
+
downcase ? s.gsub(/([a-z])([A-Z])/) {"#{$1}-#{$2}"}.downcase : s
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def version()
|
|
15
|
+
File.read(root_dir 'VERSION')[/[\d\.]+/]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def root_dir(path = '')
|
|
19
|
+
File.expand_path "../../#{path}", __dir__
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def inc_dir()
|
|
23
|
+
root_dir 'include'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def lib_dir()
|
|
27
|
+
root_dir 'lib'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ext_dir()
|
|
31
|
+
root_dir 'ext'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def lib_name()
|
|
35
|
+
"#{name true}.dll" if /mswin|ming|cygwin/.match? RUBY_PLATFORM
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end# Extension
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
end# ReflexTerminal
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- mode: ruby -*-
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
require_relative 'lib/reflex-terminal/extension'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |s|
|
|
8
|
+
glob = -> *patterns do
|
|
9
|
+
patterns.map {|pat| Dir.glob(pat).to_a}.flatten
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
ext = ReflexTerminal::Extension
|
|
13
|
+
name = ext.name true
|
|
14
|
+
rdocs = glob.call *%w[README .doc/ext/**/*.cpp]
|
|
15
|
+
|
|
16
|
+
s.name = name
|
|
17
|
+
s.version = ext.version
|
|
18
|
+
s.license = 'MIT'
|
|
19
|
+
s.summary = 'A terminal emulator view for Reflex.'
|
|
20
|
+
s.description = 'Provides Reflex::Terminal, a headless terminal emulator model built on libghostty-vt, and Reflex::TerminalView, a View that renders it in the Reflex GUI toolkit.'
|
|
21
|
+
s.authors = %w[xordog]
|
|
22
|
+
s.email = 'xordog@gmail.com'
|
|
23
|
+
s.homepage = "https://github.com/xord/reflex-terminal"
|
|
24
|
+
|
|
25
|
+
s.platform = Gem::Platform::RUBY
|
|
26
|
+
s.required_ruby_version = '>= 3.0.0'
|
|
27
|
+
|
|
28
|
+
s.add_dependency 'xot', '~> 0.4.0'
|
|
29
|
+
s.add_dependency 'rucy', '~> 0.4.0'
|
|
30
|
+
s.add_dependency 'rays', '~> 0.4.0'
|
|
31
|
+
s.add_dependency 'reflexion', '~> 0.6.0'
|
|
32
|
+
|
|
33
|
+
s.files = `git ls-files`.split $/
|
|
34
|
+
s.executables = s.files.grep(%r{^bin/}) {|f| File.basename f}
|
|
35
|
+
s.test_files = s.files.grep %r{^(test|spec|features)/}
|
|
36
|
+
s.extra_rdoc_files = rdocs.to_a
|
|
37
|
+
|
|
38
|
+
s.extensions << 'Rakefile'
|
|
39
|
+
end
|
data/src/pty.cpp
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#if defined(OSX) || defined(LINUX)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
#include "terminal.h"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
#include <errno.h>
|
|
8
|
+
#include <fcntl.h>
|
|
9
|
+
#include <signal.h>
|
|
10
|
+
#include <stdlib.h>
|
|
11
|
+
#include <unistd.h>
|
|
12
|
+
#ifdef OSX
|
|
13
|
+
#include <util.h>
|
|
14
|
+
#else
|
|
15
|
+
#include <pty.h>
|
|
16
|
+
#endif
|
|
17
|
+
#include <sys/ioctl.h>
|
|
18
|
+
#include <sys/wait.h>
|
|
19
|
+
#include <vector>
|
|
20
|
+
#include <reflex/exception.h>
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
namespace Reflex
|
|
24
|
+
{
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
struct PTY::Data
|
|
28
|
+
{
|
|
29
|
+
|
|
30
|
+
int fd = -1, pid = -1;
|
|
31
|
+
|
|
32
|
+
};// PTY::Data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
static struct winsize
|
|
36
|
+
to_winsize (int columns, int rows, int cell_width, int cell_height)
|
|
37
|
+
{
|
|
38
|
+
struct winsize size = {};
|
|
39
|
+
size.ws_col = (unsigned short) columns;
|
|
40
|
+
size.ws_row = (unsigned short) rows;
|
|
41
|
+
size.ws_xpixel = (unsigned short) (columns * cell_width);
|
|
42
|
+
size.ws_ypixel = (unsigned short) (rows * cell_height);
|
|
43
|
+
return size;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
PTY::PTY ()
|
|
48
|
+
{
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
PTY::~PTY ()
|
|
52
|
+
{
|
|
53
|
+
close();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
void
|
|
57
|
+
PTY::spawn (
|
|
58
|
+
const StringList& args, const Terminal::EnvMap& envs,
|
|
59
|
+
int columns, int rows, int cell_width, int cell_height,
|
|
60
|
+
bool login)
|
|
61
|
+
{
|
|
62
|
+
if (args.empty() || args[0].empty())
|
|
63
|
+
argument_error(__FILE__, __LINE__, "args is empty");
|
|
64
|
+
if (is_open())
|
|
65
|
+
invalid_state_error(__FILE__, __LINE__, "already spawned");
|
|
66
|
+
|
|
67
|
+
String argv0 = args[0];
|
|
68
|
+
if (login)
|
|
69
|
+
{
|
|
70
|
+
// shells read their login profiles when argv[0] starts with
|
|
71
|
+
// a '-', so pass the base name prefixed with it (/bin/zsh
|
|
72
|
+
// becomes -zsh); the path itself still goes to execvp()
|
|
73
|
+
size_t pos = argv0.rfind('/');
|
|
74
|
+
argv0 = "-" + (pos == String::npos ? argv0 : argv0.substr(pos + 1));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
std::vector<const char*> argv;
|
|
78
|
+
argv.emplace_back(argv0.c_str());
|
|
79
|
+
for (size_t i = 1; i < args.size(); ++i)
|
|
80
|
+
argv.emplace_back(args[i].c_str());
|
|
81
|
+
argv.push_back(NULL);
|
|
82
|
+
|
|
83
|
+
struct winsize size = to_winsize(columns, rows, cell_width, cell_height);
|
|
84
|
+
|
|
85
|
+
// built here rather than in the child: allocating between fork()
|
|
86
|
+
// and exec() is not async-signal-safe
|
|
87
|
+
Terminal::EnvMap child_envs = Terminal_make_child_envs(envs);
|
|
88
|
+
|
|
89
|
+
int master = -1;
|
|
90
|
+
pid_t child = forkpty(&master, NULL, NULL, &size);
|
|
91
|
+
if (child < 0)
|
|
92
|
+
system_error(__FILE__, __LINE__, "forkpty() failed");
|
|
93
|
+
|
|
94
|
+
if (child == 0)
|
|
95
|
+
{
|
|
96
|
+
// child process: exec the command
|
|
97
|
+
for (const auto& it : child_envs)
|
|
98
|
+
{
|
|
99
|
+
if (it.second)
|
|
100
|
+
setenv(it.first.c_str(), it.second->c_str(), 1);
|
|
101
|
+
else
|
|
102
|
+
unsetenv(it.first.c_str());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// exec the real path; argv[0] may be the login shell name
|
|
106
|
+
execvp(args[0].c_str(), (char* const*) &argv[0]);
|
|
107
|
+
_exit(127);// exec failed
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
int flags = fcntl(master, F_GETFL, 0);
|
|
111
|
+
if (flags < 0 || fcntl(master, F_SETFL, flags | O_NONBLOCK) < 0)
|
|
112
|
+
{
|
|
113
|
+
::close(master);
|
|
114
|
+
kill(child, SIGKILL);
|
|
115
|
+
waitpid(child, NULL, 0);
|
|
116
|
+
system_error(__FILE__, __LINE__, "failed to set O_NONBLOCK");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
self->fd = master;
|
|
120
|
+
self->pid = child;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
size_t
|
|
124
|
+
PTY::read (char* buffer, size_t size)
|
|
125
|
+
{
|
|
126
|
+
// nonblocking: returns 0 when nothing is available, which a caller
|
|
127
|
+
// cannot tell apart from the child having exited -- is_child_alive()
|
|
128
|
+
// answers that
|
|
129
|
+
|
|
130
|
+
if (!is_open()) return 0;
|
|
131
|
+
|
|
132
|
+
ssize_t n = ::read(self->fd, buffer, size);
|
|
133
|
+
if (n > 0) return (size_t) n;
|
|
134
|
+
|
|
135
|
+
if (n == 0 || (errno != EAGAIN && errno != EINTR))
|
|
136
|
+
close();// EOF or EIO: the child has exited
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
bool
|
|
141
|
+
PTY::wait_readable (int timeout_msec) const
|
|
142
|
+
{
|
|
143
|
+
if (!is_open()) return false;
|
|
144
|
+
|
|
145
|
+
fd_set fds;
|
|
146
|
+
FD_ZERO(&fds);
|
|
147
|
+
FD_SET(self->fd, &fds);
|
|
148
|
+
struct timeval timeout = {0, timeout_msec * 1000};
|
|
149
|
+
return select(self->fd + 1, &fds, NULL, NULL, &timeout) > 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
void
|
|
153
|
+
PTY::write (const char* bytes, size_t size)
|
|
154
|
+
{
|
|
155
|
+
if (!is_open() || !bytes || size == 0) return;
|
|
156
|
+
|
|
157
|
+
size_t offset = 0;
|
|
158
|
+
while (offset < size)
|
|
159
|
+
{
|
|
160
|
+
ssize_t n = ::write(self->fd, bytes + offset, size - offset);
|
|
161
|
+
if (n > 0)
|
|
162
|
+
offset += (size_t) n;
|
|
163
|
+
else if (errno == EINTR)
|
|
164
|
+
continue;
|
|
165
|
+
else if (errno == EAGAIN)
|
|
166
|
+
{
|
|
167
|
+
// the pty buffer is full; wait briefly for the child to drain it
|
|
168
|
+
fd_set fds;
|
|
169
|
+
FD_ZERO(&fds);
|
|
170
|
+
FD_SET(self->fd, &fds);
|
|
171
|
+
struct timeval timeout = {0, 100 * 1000};// 100ms
|
|
172
|
+
if (select(self->fd + 1, NULL, &fds, NULL, &timeout) <= 0)
|
|
173
|
+
break;// give up to avoid blocking the UI thread forever
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
{
|
|
177
|
+
close();
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
void
|
|
184
|
+
PTY::set_size (int columns, int rows, int cell_width, int cell_height)
|
|
185
|
+
{
|
|
186
|
+
if (!is_open()) return;
|
|
187
|
+
|
|
188
|
+
struct winsize size = to_winsize(columns, rows, cell_width, cell_height);
|
|
189
|
+
ioctl(self->fd, TIOCSWINSZ, &size);// sends SIGWINCH to the child
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
void
|
|
193
|
+
PTY::close ()
|
|
194
|
+
{
|
|
195
|
+
if (self->fd >= 0)
|
|
196
|
+
{
|
|
197
|
+
::close(self->fd);
|
|
198
|
+
self->fd = -1;
|
|
199
|
+
}
|
|
200
|
+
if (self->pid >= 0)
|
|
201
|
+
{
|
|
202
|
+
waitpid(self->pid, NULL, WNOHANG);// reap if already exited
|
|
203
|
+
self->pid = -1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
bool
|
|
208
|
+
PTY::is_open () const
|
|
209
|
+
{
|
|
210
|
+
return self->fd >= 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
bool
|
|
214
|
+
PTY::is_child_alive () const
|
|
215
|
+
{
|
|
216
|
+
if (self->pid < 0) return false;
|
|
217
|
+
|
|
218
|
+
return waitpid(self->pid, NULL, WNOHANG) == 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
PTY::operator bool () const
|
|
222
|
+
{
|
|
223
|
+
return is_open();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
bool
|
|
227
|
+
PTY::operator ! () const
|
|
228
|
+
{
|
|
229
|
+
return !operator bool();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
}// Reflex
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
#endif// OSX || LINUX
|