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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9aca479c6fefe50cf31a19a4d9f2ee2e9ccda43c41737accfc7d4297fd68df34
4
- data.tar.gz: e877aa139fc517da68b0214e3b76e9f0b87d7d8277cc096370c451153a8463ca
3
+ metadata.gz: c614cedbb2f3c547a9338aab4e19ff9861d5da64a1688a34bb29b2f00f3c5781
4
+ data.tar.gz: 297e80f11061fae2596697f78f0e19d97d6537c6133999c2df70b1f277e53292
5
5
  SHA512:
6
- metadata.gz: 264107d5d55ddb7f21f95c973c6211113eebe67e25b64c24c3c1b5810b31b93231b7e6297e9c5143fd39f3d3496f66e7539e8a43863e379cbdd0fefa4279fbf6
7
- data.tar.gz: b6af96ac20098f3b52aa9642be6550aa35a4c0a37cc0ab9cf5775f86d0e717e819e3dcbb9dbb509f63fb324761ae1b4e2b4019ac9c7fb7a3717b0859b3104dbf
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
- # Tarazed
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
- ## Usage
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
- Run `bundle install`, then `bundle exec rake test`. Validate the signatures with
64
- `bundle exec rbs -I sig validate` and run the benchmark with
65
- `BUDGET=1 bundle exec rake bench`.
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
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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, :status, :initial_cwd, :command_name
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tarazed
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -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
- private_constant :P, :I, :U, :N, :V
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
- check_hresult(function(:ResizePseudoConsole, [P, I], I).call(@console, coordinate(columns, rows)))
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 alive?
108
- !@closed && function(:WaitForSingleObject, [P, U], U).call(@process, 0) == 258
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
- return self if @closed
113
-
114
- @closed = true
115
- close_handle(@input_write)
116
- @input_write = nil
117
- close_console
118
- @reader&.join(2)
119
- close_handle(@output_read)
120
- close_handle(@process)
121
- @output_read = @process = nil
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
- return unless @console
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(@console)
220
- @console = nil
280
+ function(:ClosePseudoConsole, [P], V, need_gvl: false).call(console)
221
281
  end
222
282
 
223
283
  def release
224
- safely { close_handle(@input_write) }
225
- safely { close_console }
226
- @reader&.join(2)
227
- safely { close_handle(@output_read) }
228
- safely { close_handle(@process) }
229
- @input_write = @output_read = @process = nil
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: Process::Status?
148
+ attr_reader status: _PTYStatus?
142
149
  attr_reader initial_cwd: String
143
150
  attr_reader command_name: String
144
151
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tarazed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada