tarazed 0.2.0 → 0.2.2
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/CHANGELOG.md +8 -0
- data/README.md +52 -7
- data/lib/tarazed/pty.rb +3 -1
- data/lib/tarazed/version.rb +1 -1
- data/lib/tarazed/windows/conpty.rb +87 -23
- data/sig/tarazed.rbs +8 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c614cedbb2f3c547a9338aab4e19ff9861d5da64a1688a34bb29b2f00f3c5781
|
|
4
|
+
data.tar.gz: 297e80f11061fae2596697f78f0e19d97d6537c6133999c2df70b1f277e53292
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 724ba37a3d343f37740d857d00cc38012d338d588cae81ddde319c0e7b362f6d88a6149abc991a283980366c471b6f4b70b87c3d6aa9a9a6d3e61097245dec24
|
|
7
|
+
data.tar.gz: 787a10f3d001c3bae7501c23037c75ce53488749a3fecd2cdd9671f8d46fabe3d08c36503d89a18cf7c204a1465c8049e4537dc6353fca591d626744da7e2593
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.2 - 2026-09-17
|
|
6
|
+
|
|
7
|
+
- Drain final Windows ConPTY output to EOF after the primary process exits without racing resize or close.
|
|
8
|
+
|
|
9
|
+
## 0.2.1 - 2026-09-16
|
|
10
|
+
|
|
11
|
+
- Report and cache natural Windows ConPTY process exit status through the cross-platform `PTY#status` API.
|
|
12
|
+
|
|
5
13
|
## 0.2.0 - 2026-09-16
|
|
6
14
|
|
|
7
15
|
- Track OSC 133 command boundaries, input, output rows, exit status, and OSC 7 working directories in bounded history.
|
data/README.md
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">Tarazed</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Pure Ruby terminal emulator core</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/tarazed"><img src="https://img.shields.io/gem/v/tarazed.svg?colorB=319e8c" alt="Gem Version"></a>
|
|
9
|
+
<a href="https://rubygems.org/gems/tarazed"><img src="https://img.shields.io/gem/dt/tarazed.svg" alt="Downloads"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.1-ruby.svg" alt="Ruby Version">
|
|
11
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="#features">Features</a> ·
|
|
16
|
+
<a href="#installation">Installation</a> ·
|
|
17
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
18
|
+
<a href="#pty-sessions">PTY Sessions</a> ·
|
|
19
|
+
<a href="#command-history">Command History</a> ·
|
|
20
|
+
<a href="#shell-integration">Shell Integration</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
2
24
|
|
|
3
25
|
Tarazed is a pure Ruby terminal emulator core. It provides the
|
|
4
26
|
terminal cell grid, scrollback, VT parser, keyboard/mouse encoding, and a
|
|
5
27
|
POSIX or Windows ConPTY session without depending on an editor or UI toolkit.
|
|
6
28
|
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Unicode-aware cell grid with bounded scrollback
|
|
32
|
+
- Chunk-safe VT parsing for terminal controls, input, links, and selection
|
|
33
|
+
- Keyboard, mouse, bracketed-paste, and terminal-reply encoding
|
|
34
|
+
- POSIX PTY and 64-bit Windows ConPTY sessions behind one API
|
|
35
|
+
- OSC 133 command history and OSC 7 working-directory tracking
|
|
36
|
+
- Packaged shell integration for bash, zsh, and fish
|
|
37
|
+
|
|
7
38
|
## Installation
|
|
8
39
|
|
|
9
40
|
```bash
|
|
@@ -12,7 +43,9 @@ bundle add tarazed
|
|
|
12
43
|
|
|
13
44
|
Or run `gem install tarazed`.
|
|
14
45
|
|
|
15
|
-
|
|
46
|
+
Tarazed supports Ruby 3.1 and later.
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
16
49
|
|
|
17
50
|
Feed arbitrary byte chunks into a `VT`; incomplete UTF-8 and control sequences
|
|
18
51
|
are retained until the next chunk.
|
|
@@ -26,6 +59,8 @@ terminal.feed("\e[32mready\e[0m\r\n")
|
|
|
26
59
|
puts screen.text
|
|
27
60
|
```
|
|
28
61
|
|
|
62
|
+
## PTY Sessions
|
|
63
|
+
|
|
29
64
|
`Tarazed::PTY` owns a child process and feeds its output into the same grid.
|
|
30
65
|
It uses a POSIX PTY on macOS and Linux and ConPTY on 64-bit Windows:
|
|
31
66
|
|
|
@@ -33,11 +68,15 @@ It uses a POSIX PTY on macOS and Linux and ConPTY on 64-bit Windows:
|
|
|
33
68
|
terminal = Tarazed::PTY.new(command: ["/bin/sh"], columns: 80, rows: 24)
|
|
34
69
|
terminal.write("printf 'hello\\n'\r")
|
|
35
70
|
terminal.read(timeout: 0.1)
|
|
71
|
+
puts terminal.status.exitstatus if !terminal.alive? && terminal.status
|
|
36
72
|
terminal.close
|
|
37
73
|
```
|
|
38
74
|
|
|
39
75
|
ConPTY requires a supported 64-bit Windows release and does not fall back to a
|
|
40
|
-
pipe-only console.
|
|
76
|
+
pipe-only console. After natural process exit, `status` provides `exited?`,
|
|
77
|
+
`exitstatus`, `success?`, and `termsig` on every platform.
|
|
78
|
+
|
|
79
|
+
## Command History
|
|
41
80
|
|
|
42
81
|
`Tarazed::Session` adds a pump-oriented API and bounded OSC 133 command
|
|
43
82
|
history. Command rows and output ranges use stable absolute history rows, and
|
|
@@ -52,6 +91,8 @@ end
|
|
|
52
91
|
session.close
|
|
53
92
|
```
|
|
54
93
|
|
|
94
|
+
## Shell Integration
|
|
95
|
+
|
|
55
96
|
Shell integration snippets for bash, zsh, and fish are packaged with the gem.
|
|
56
97
|
An embedding application can inject one into a new interactive shell with
|
|
57
98
|
`Tarazed::ShellIntegration.read(:bash)` (or `:zsh` / `:fish`). The snippets
|
|
@@ -60,9 +101,13 @@ visible prompt.
|
|
|
60
101
|
|
|
61
102
|
## Development
|
|
62
103
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
104
|
+
```bash
|
|
105
|
+
bundle install
|
|
106
|
+
bundle exec rake test
|
|
107
|
+
bundle exec rbs -I sig validate
|
|
108
|
+
BUDGET=1 bundle exec rake bench
|
|
109
|
+
gem build --strict tarazed.gemspec
|
|
110
|
+
```
|
|
66
111
|
|
|
67
112
|
## Contributing
|
|
68
113
|
|
|
@@ -70,4 +115,4 @@ Bug reports and pull requests are welcome at https://github.com/noxdea/tarazed.
|
|
|
70
115
|
|
|
71
116
|
## License
|
|
72
117
|
|
|
73
|
-
|
|
118
|
+
Tarazed is available under the [MIT License](LICENSE.txt).
|
data/lib/tarazed/pty.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "shellwords"
|
|
|
5
5
|
|
|
6
6
|
module Tarazed
|
|
7
7
|
class PTY
|
|
8
|
-
attr_reader :reader, :writer, :pid, :grid, :vt, :
|
|
8
|
+
attr_reader :reader, :writer, :pid, :grid, :vt, :initial_cwd, :command_name
|
|
9
9
|
|
|
10
10
|
def initialize(command: ENV.fetch("SHELL", "/bin/sh"), cwd: Dir.pwd, columns: 80, rows: 24, env: {},
|
|
11
11
|
scrollback: 10_000, queue_limit_bytes: 8_388_608, command_history_limit: 1_000, on_command: nil)
|
|
@@ -124,6 +124,8 @@ module Tarazed
|
|
|
124
124
|
@foreground_name = nil
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
def status = @native ? @native.status : @status
|
|
128
|
+
|
|
127
129
|
def signal(name = "INT")
|
|
128
130
|
return @native.write("\x03") if @native && name == "INT"
|
|
129
131
|
if @native
|
data/lib/tarazed/version.rb
CHANGED
|
@@ -21,11 +21,30 @@ module Tarazed
|
|
|
21
21
|
class ConPTY
|
|
22
22
|
P, I, U, N, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT, Fiddle::TYPE_SIZE_T,
|
|
23
23
|
Fiddle::TYPE_VOID
|
|
24
|
-
|
|
24
|
+
STILL_ACTIVE = 259
|
|
25
|
+
WAIT_OBJECT_0 = 0
|
|
26
|
+
WAIT_TIMEOUT = 258
|
|
27
|
+
EXIT_POLL_MILLISECONDS = 10
|
|
28
|
+
private_constant :P, :I, :U, :N, :V, :WAIT_OBJECT_0, :WAIT_TIMEOUT, :EXIT_POLL_MILLISECONDS
|
|
29
|
+
|
|
30
|
+
class Status
|
|
31
|
+
attr_reader :exitstatus
|
|
32
|
+
|
|
33
|
+
def initialize(exitstatus)
|
|
34
|
+
@exitstatus = exitstatus
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def exited? = true
|
|
39
|
+
def success? = exitstatus.zero?
|
|
40
|
+
def termsig = nil
|
|
41
|
+
end
|
|
42
|
+
private_constant :Status
|
|
25
43
|
|
|
26
44
|
attr_reader :pid
|
|
27
45
|
|
|
28
46
|
def initialize(command: nil, columns: 80, rows: 24, cwd: nil, env: {}, kernel: nil)
|
|
47
|
+
@console_lock, @lifecycle_lock = Mutex.new, Mutex.new
|
|
29
48
|
raise Error, "ConPTY requires 64-bit Windows Ruby" unless Fiddle::SIZEOF_VOIDP == 8
|
|
30
49
|
|
|
31
50
|
@kernel = kernel || Library.new("kernel32.dll")
|
|
@@ -40,6 +59,7 @@ module Tarazed
|
|
|
40
59
|
attributes = process_attributes(@console)
|
|
41
60
|
@process, @pid = spawn(command, cwd, env, attributes)
|
|
42
61
|
start_reader
|
|
62
|
+
start_exit_watcher
|
|
43
63
|
rescue Fiddle::DLError, LoadError => error
|
|
44
64
|
release
|
|
45
65
|
raise Error, "ConPTY is unavailable: #{error.message}"
|
|
@@ -101,24 +121,36 @@ module Tarazed
|
|
|
101
121
|
end
|
|
102
122
|
|
|
103
123
|
def resize(columns, rows)
|
|
104
|
-
|
|
124
|
+
size = coordinate(columns, rows)
|
|
125
|
+
@console_lock.synchronize do
|
|
126
|
+
return unless @console
|
|
127
|
+
|
|
128
|
+
check_hresult(function(:ResizePseudoConsole, [P, I], I).call(@console, size))
|
|
129
|
+
end
|
|
105
130
|
end
|
|
106
131
|
|
|
107
|
-
def
|
|
108
|
-
|
|
132
|
+
def status
|
|
133
|
+
refresh_status unless @closed
|
|
134
|
+
@status
|
|
109
135
|
end
|
|
110
136
|
|
|
137
|
+
def alive? = !@closed && !status
|
|
138
|
+
|
|
111
139
|
def close
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
@lifecycle_lock.synchronize do
|
|
141
|
+
return self if @closed
|
|
142
|
+
|
|
143
|
+
safely { refresh_status }
|
|
144
|
+
@closed = true
|
|
145
|
+
close_handle(@input_write)
|
|
146
|
+
@input_write = nil
|
|
147
|
+
close_console
|
|
148
|
+
@watcher&.join
|
|
149
|
+
@reader&.join(2)
|
|
150
|
+
close_handle(@output_read)
|
|
151
|
+
close_handle(@process)
|
|
152
|
+
@output_read = @process = nil
|
|
153
|
+
end
|
|
122
154
|
self
|
|
123
155
|
end
|
|
124
156
|
|
|
@@ -128,6 +160,15 @@ module Tarazed
|
|
|
128
160
|
@kernel.fn(name, arguments, result, need_gvl: need_gvl)
|
|
129
161
|
end
|
|
130
162
|
|
|
163
|
+
def refresh_status
|
|
164
|
+
return @status if @status
|
|
165
|
+
|
|
166
|
+
code = [0].pack("I")
|
|
167
|
+
check(function(:GetExitCodeProcess, [P, P], I).call(@process, code), "GetExitCodeProcess")
|
|
168
|
+
value = code.unpack1("I")
|
|
169
|
+
@status = Status.new(value) unless value == STILL_ACTIVE
|
|
170
|
+
end
|
|
171
|
+
|
|
131
172
|
def process_attributes(console)
|
|
132
173
|
size = [0].pack("J")
|
|
133
174
|
function(:InitializeProcThreadAttributeList, [P, U, U, P], I).call(0, 1, 0, size)
|
|
@@ -213,20 +254,43 @@ module Tarazed
|
|
|
213
254
|
end
|
|
214
255
|
end
|
|
215
256
|
|
|
257
|
+
def start_exit_watcher
|
|
258
|
+
wait = function(:WaitForSingleObject, [P, U], U, need_gvl: false)
|
|
259
|
+
@watcher = Thread.new do
|
|
260
|
+
loop do
|
|
261
|
+
result = wait.call(@process, EXIT_POLL_MILLISECONDS)
|
|
262
|
+
break if @closed
|
|
263
|
+
if result == WAIT_OBJECT_0
|
|
264
|
+
close_console
|
|
265
|
+
break
|
|
266
|
+
end
|
|
267
|
+
raise SystemCallError.new("WaitForSingleObject", Fiddle.last_error) unless result == WAIT_TIMEOUT
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
216
272
|
def close_console
|
|
217
|
-
|
|
273
|
+
console = @console_lock.synchronize do
|
|
274
|
+
value = @console
|
|
275
|
+
@console = nil
|
|
276
|
+
value
|
|
277
|
+
end
|
|
278
|
+
return unless console
|
|
218
279
|
|
|
219
|
-
function(:ClosePseudoConsole, [P], V, need_gvl: false).call(
|
|
220
|
-
@console = nil
|
|
280
|
+
function(:ClosePseudoConsole, [P], V, need_gvl: false).call(console)
|
|
221
281
|
end
|
|
222
282
|
|
|
223
283
|
def release
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
284
|
+
@lifecycle_lock.synchronize do
|
|
285
|
+
@closed = true
|
|
286
|
+
safely { close_handle(@input_write) }
|
|
287
|
+
safely { close_console }
|
|
288
|
+
@watcher&.join
|
|
289
|
+
@reader&.join(2)
|
|
290
|
+
safely { close_handle(@output_read) }
|
|
291
|
+
safely { close_handle(@process) }
|
|
292
|
+
@input_write = @output_read = @process = nil
|
|
293
|
+
end
|
|
230
294
|
end
|
|
231
295
|
|
|
232
296
|
def safely
|
data/sig/tarazed.rbs
CHANGED
|
@@ -132,13 +132,20 @@ module Tarazed
|
|
|
132
132
|
def key: (String | Symbol name, ?shift: bool, ?alt: bool, ?control: bool) -> String
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
interface _PTYStatus
|
|
136
|
+
def exited?: () -> bool
|
|
137
|
+
def exitstatus: () -> Integer?
|
|
138
|
+
def success?: () -> bool?
|
|
139
|
+
def termsig: () -> Integer?
|
|
140
|
+
end
|
|
141
|
+
|
|
135
142
|
class PTY
|
|
136
143
|
attr_reader reader: IO?
|
|
137
144
|
attr_reader writer: IO?
|
|
138
145
|
attr_reader pid: Integer
|
|
139
146
|
attr_reader grid: Grid
|
|
140
147
|
attr_reader vt: VT
|
|
141
|
-
attr_reader status:
|
|
148
|
+
attr_reader status: _PTYStatus?
|
|
142
149
|
attr_reader initial_cwd: String
|
|
143
150
|
attr_reader command_name: String
|
|
144
151
|
|