tty-command-window 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/CHANGELOG.md +54 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/lib/tty/command/window/ansi.rb +112 -0
- data/lib/tty/command/window/block.rb +184 -0
- data/lib/tty/command/window/child_session.rb +161 -0
- data/lib/tty/command/window/coordinator.rb +315 -0
- data/lib/tty/command/window/emulator.rb +613 -0
- data/lib/tty/command/window/input_router.rb +249 -0
- data/lib/tty/command/window/integration.rb +195 -0
- data/lib/tty/command/window/runner.rb +245 -0
- data/lib/tty/command/window/spinner.rb +30 -0
- data/lib/tty/command/window/trap_manager.rb +117 -0
- data/lib/tty/command/window/version.rb +9 -0
- data/lib/tty/command/window/window_options.rb +76 -0
- data/lib/tty/command/window.rb +110 -0
- data/lib/tty-command-window.rb +3 -0
- metadata +116 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TTY
|
|
4
|
+
class Command
|
|
5
|
+
module Window
|
|
6
|
+
# Braille spinner used in block title bars.
|
|
7
|
+
class Spinner
|
|
8
|
+
FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@index = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Advance and return the next frame.
|
|
15
|
+
#
|
|
16
|
+
# @return [String]
|
|
17
|
+
def tick
|
|
18
|
+
frame = FRAMES[@index]
|
|
19
|
+
@index = (@index + 1) % FRAMES.length
|
|
20
|
+
frame
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String] the current frame without advancing
|
|
24
|
+
def frame
|
|
25
|
+
FRAMES[@index]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TTY
|
|
4
|
+
class Command
|
|
5
|
+
module Window
|
|
6
|
+
# Process-global owner of the signals this gem cares about
|
|
7
|
+
# (+WINCH+, +INT+, +TERM+, +HUP+).
|
|
8
|
+
#
|
|
9
|
+
# Ruby's +Signal.trap+ is a process singleton: two coordinators
|
|
10
|
+
# installing their own trap for the same signal will silently
|
|
11
|
+
# clobber each other and lose the host application's previous
|
|
12
|
+
# handler. TrapManager mediates that by installing exactly one
|
|
13
|
+
# trap per signal (the first time anyone subscribes) and
|
|
14
|
+
# multiplexing to a subscriber list.
|
|
15
|
+
#
|
|
16
|
+
# **Ownership contract.** Once a signal has been installed by
|
|
17
|
+
# TrapManager it remains installed for the life of the process
|
|
18
|
+
# (only {.reset!}, which is test-only, uninstalls). This avoids
|
|
19
|
+
# clobbering a host-application handler that a caller installed
|
|
20
|
+
# *after* our first subscribe (follow-up review P1-6): restoring
|
|
21
|
+
# the snapshot we captured at first-install time would silently
|
|
22
|
+
# overwrite the host's newer handler.
|
|
23
|
+
#
|
|
24
|
+
# Thread-safe. Trap-safe: the trap block reads a reference to
|
|
25
|
+
# the subscriber array under MRI's GVL and iterates a local
|
|
26
|
+
# snapshot; unsubscribe swaps the array reference so an
|
|
27
|
+
# in-flight trap keeps iterating the array it saw.
|
|
28
|
+
class TrapManager
|
|
29
|
+
@mutex = Mutex.new
|
|
30
|
+
@subscribers = {} # signal (String) => [callable, ...]
|
|
31
|
+
@originals = {} # signal (String) => Proc / String / nil returned by Signal.trap
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Register +callable+ as a handler for +signal+.
|
|
35
|
+
#
|
|
36
|
+
# @param signal [String, Symbol] e.g. "INT", "TERM", "WINCH"
|
|
37
|
+
# @param callable [#call] invoked as +callable.call(signal)+ in trap context
|
|
38
|
+
# @return [#call] the subscribed callable (pass back to {.unsubscribe})
|
|
39
|
+
def subscribe(signal, callable)
|
|
40
|
+
key = signal.to_s
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
# Populate the subscriber list BEFORE installing the trap
|
|
43
|
+
# so a signal arriving between Signal.trap returning and
|
|
44
|
+
# the append cannot observe an empty list (follow-up
|
|
45
|
+
# review P1-7).
|
|
46
|
+
list = @subscribers[key] || []
|
|
47
|
+
@subscribers[key] = list + [callable]
|
|
48
|
+
install_once(key) unless @originals.key?(key)
|
|
49
|
+
end
|
|
50
|
+
callable
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Remove a previously subscribed handler.
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
def unsubscribe(signal, callable)
|
|
57
|
+
key = signal.to_s
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
list = @subscribers[key]
|
|
60
|
+
next if list.nil?
|
|
61
|
+
|
|
62
|
+
new_list = list.reject { |c| c.equal?(callable) }
|
|
63
|
+
# Keep the installed trap even when the subscriber list
|
|
64
|
+
# empties: uninstalling would restore the snapshot we took
|
|
65
|
+
# at first-install time and clobber a host handler
|
|
66
|
+
# installed after that snapshot (follow-up review P1-6).
|
|
67
|
+
# reset! (test-only) is the one path that uninstalls.
|
|
68
|
+
@subscribers[key] = new_list
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Uninstall every subscriber and restore original handlers.
|
|
73
|
+
# Intended for tests and emergency teardown.
|
|
74
|
+
def reset!
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
# Materialize keys before iteration because uninstall mutates @originals.
|
|
77
|
+
@subscribers.keys.to_a.each { |key| uninstall(key) }
|
|
78
|
+
@subscribers.clear
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @api private for tests
|
|
83
|
+
def subscribed?(signal)
|
|
84
|
+
@mutex.synchronize { !(@subscribers[signal.to_s] || []).empty? }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def install_once(signal)
|
|
90
|
+
@originals[signal] = Signal.trap(signal) do
|
|
91
|
+
# Trap context: no mutex, no allocation-heavy work.
|
|
92
|
+
subs = @subscribers[signal] || []
|
|
93
|
+
subs.each do |sub|
|
|
94
|
+
sub.call(signal)
|
|
95
|
+
rescue StandardError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
orig = @originals[signal]
|
|
99
|
+
orig.call if orig.respond_to?(:call)
|
|
100
|
+
end
|
|
101
|
+
rescue ArgumentError
|
|
102
|
+
# Reserved signal (e.g. running under a debugger). Leave @originals[signal] unset;
|
|
103
|
+
# subscribers still get called from any trap someone else might install.
|
|
104
|
+
@originals[signal] = nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def uninstall(signal)
|
|
108
|
+
original = @originals.delete(signal)
|
|
109
|
+
Signal.trap(signal, original || "DEFAULT")
|
|
110
|
+
rescue ArgumentError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TTY
|
|
4
|
+
class Command
|
|
5
|
+
module Window
|
|
6
|
+
# Immutable, validated configuration for a single windowed run.
|
|
7
|
+
#
|
|
8
|
+
# Built from the raw options hash the user passed to +run_windowed+
|
|
9
|
+
# by {.build}. Runner and Block consume named accessors on this
|
|
10
|
+
# object instead of dereferencing symbol keys, so the option
|
|
11
|
+
# contract is explicit.
|
|
12
|
+
#
|
|
13
|
+
# +Struct.new(..., keyword_init: true)+ is used (rather than
|
|
14
|
+
# +Data.define+) because +Data.define+ ships in Ruby 3.2 and the
|
|
15
|
+
# gemspec's +required_ruby_version+ floor is 3.1. Instances are
|
|
16
|
+
# frozen in {.build} for immutability.
|
|
17
|
+
WindowOptions = Struct.new(
|
|
18
|
+
:lines, :title, :on_exit, :capture, :capture_max_bytes,
|
|
19
|
+
:scrollback, :output_log, :interactive,
|
|
20
|
+
keyword_init: true
|
|
21
|
+
) do
|
|
22
|
+
# @param window_options [Hash] the raw window options subset
|
|
23
|
+
# returned by {Window.split_options}
|
|
24
|
+
# @param cmd [TTY::Command::Cmd] used to derive a default title
|
|
25
|
+
# @return [WindowOptions]
|
|
26
|
+
def self.build(window_options, cmd)
|
|
27
|
+
lines = Integer(window_options.fetch(:lines, DEFAULT_LINES))
|
|
28
|
+
raise ArgumentError, "lines must be >= 1" if lines < 1
|
|
29
|
+
|
|
30
|
+
on_exit = validated_enum(window_options, :on_exit, ON_EXIT_MODES, :freeze)
|
|
31
|
+
capture = validated_enum(window_options, :capture, CAPTURE_MODES, :raw)
|
|
32
|
+
|
|
33
|
+
capture_max_bytes = window_options.fetch(:capture_max_bytes, DEFAULT_CAPTURE_MAX_BYTES)
|
|
34
|
+
unless capture_max_bytes.nil?
|
|
35
|
+
capture_max_bytes = Integer(capture_max_bytes)
|
|
36
|
+
raise ArgumentError, "capture_max_bytes must be >= 1 or nil" if capture_max_bytes < 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Window.validate_on_unavailable!(window_options)
|
|
40
|
+
|
|
41
|
+
title = window_options.fetch(:title, nil)
|
|
42
|
+
title = cmd.to_command if title.nil?
|
|
43
|
+
|
|
44
|
+
new(
|
|
45
|
+
lines: lines,
|
|
46
|
+
title: title,
|
|
47
|
+
on_exit: on_exit,
|
|
48
|
+
capture: capture,
|
|
49
|
+
capture_max_bytes: capture_max_bytes,
|
|
50
|
+
scrollback: Integer(window_options.fetch(:scrollback, DEFAULT_SCROLLBACK)),
|
|
51
|
+
output_log: window_options[:output_log],
|
|
52
|
+
interactive: window_options[:interactive] == true
|
|
53
|
+
).freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.validated_enum(window_options, key, modes, default)
|
|
57
|
+
value = window_options.fetch(key, default)
|
|
58
|
+
unless modes.include?(value)
|
|
59
|
+
raise ArgumentError, "#{key} must be one of #{modes.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
private_class_method :validated_enum
|
|
65
|
+
|
|
66
|
+
def interactive?
|
|
67
|
+
interactive
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def capture_screen?
|
|
71
|
+
capture == :screen
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require "tty/command"
|
|
5
|
+
|
|
6
|
+
require_relative "window/version"
|
|
7
|
+
require_relative "window/ansi"
|
|
8
|
+
require_relative "window/emulator"
|
|
9
|
+
require_relative "window/spinner"
|
|
10
|
+
require_relative "window/child_session"
|
|
11
|
+
require_relative "window/block"
|
|
12
|
+
require_relative "window/trap_manager"
|
|
13
|
+
require_relative "window/coordinator"
|
|
14
|
+
require_relative "window/input_router"
|
|
15
|
+
require_relative "window/window_options"
|
|
16
|
+
require_relative "window/runner"
|
|
17
|
+
require_relative "window/integration"
|
|
18
|
+
|
|
19
|
+
module TTY
|
|
20
|
+
class Command
|
|
21
|
+
# Namespace for the windowed-run extension.
|
|
22
|
+
#
|
|
23
|
+
# Requiring this file adds {TTY::Command#run_windowed} and
|
|
24
|
+
# {TTY::Command#run_windowed!} to +TTY::Command+.
|
|
25
|
+
module Window
|
|
26
|
+
DEFAULT_LINES = 5
|
|
27
|
+
DEFAULT_SCROLLBACK = 10_000
|
|
28
|
+
DEFAULT_CAPTURE_MAX_BYTES = 10 * 1024 * 1024
|
|
29
|
+
|
|
30
|
+
# Option keys consumed by run_windowed and stripped before any
|
|
31
|
+
# delegation to plain tty-command.
|
|
32
|
+
OPTION_KEYS = %i[
|
|
33
|
+
lines title on_exit scrollback output_log interactive capture
|
|
34
|
+
capture_max_bytes output window width on_unavailable tty
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
ON_EXIT_MODES = %i[freeze dump_on_failure collapse].freeze
|
|
38
|
+
CAPTURE_MODES = %i[raw stripped screen].freeze
|
|
39
|
+
ON_UNAVAILABLE_MODES = %i[fallback raise].freeze
|
|
40
|
+
|
|
41
|
+
# Raised by +run_windowed+ / +run_windowed!+ when
|
|
42
|
+
# +on_unavailable: :raise+ is set and the environment cannot support
|
|
43
|
+
# windowed rendering (no TTY / PTY, on Windows, or in dry-run mode).
|
|
44
|
+
class Unavailable < StandardError; end
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
# Treat non-TTY outputs as terminals.
|
|
48
|
+
#
|
|
49
|
+
# @api private Intended for test suites that render into a StringIO.
|
|
50
|
+
# Prefer per-call +window: true+ or the scoped
|
|
51
|
+
# {.with_assume_tty} block form; unscoped writes leak across
|
|
52
|
+
# examples under +config.order = :random+.
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
attr_accessor :assume_tty
|
|
55
|
+
|
|
56
|
+
# Set {.assume_tty} for the duration of the block and restore the
|
|
57
|
+
# previous value on exit — including on exception.
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
# @param value [Boolean]
|
|
61
|
+
# @yieldreturn [Object] the block's return value is returned
|
|
62
|
+
def with_assume_tty(value = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
63
|
+
previous = assume_tty
|
|
64
|
+
self.assume_tty = value
|
|
65
|
+
yield
|
|
66
|
+
ensure
|
|
67
|
+
self.assume_tty = previous
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Boolean] true when running on Windows (no PTY support)
|
|
71
|
+
def windows?
|
|
72
|
+
!!(RbConfig::CONFIG["host_os"] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Boolean] true when the PTY library is usable
|
|
76
|
+
def pty_available?
|
|
77
|
+
return @pty_available unless @pty_available.nil?
|
|
78
|
+
|
|
79
|
+
@pty_available =
|
|
80
|
+
begin
|
|
81
|
+
require "pty"
|
|
82
|
+
true
|
|
83
|
+
rescue LoadError
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Whether a windowed run can render onto the given IO.
|
|
89
|
+
#
|
|
90
|
+
# @param output [IO]
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
def renderable?(output)
|
|
93
|
+
return false if windows? || !pty_available?
|
|
94
|
+
|
|
95
|
+
assume_tty || (output.respond_to?(:tty?) && output.tty?)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Strip ANSI escape sequences (CSI, OSC, DCS, simple escapes) from a string.
|
|
99
|
+
#
|
|
100
|
+
# @param text [String]
|
|
101
|
+
# @return [String]
|
|
102
|
+
def strip_ansi(text)
|
|
103
|
+
ANSI.strip(text)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
self.assume_tty = false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tty-command-window
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michal Matyas
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: pastel
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.8'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.8'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: tty-command
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.10'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.10'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: unicode-display_width
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
- - "<"
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '4.0'
|
|
51
|
+
type: :runtime
|
|
52
|
+
prerelease: false
|
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '2.0'
|
|
58
|
+
- - "<"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '4.0'
|
|
61
|
+
description: 'Extends tty-command with run_windowed: the child process runs in a PTY
|
|
62
|
+
that reports N rows, its output (including cursor-movement escape codes) is interpreted
|
|
63
|
+
by a built-in terminal emulator and rendered as a static N-line block that streams
|
|
64
|
+
in place. Supports stacked concurrent windows, interactive stdin with focus switching,
|
|
65
|
+
resize handling and failure dumps.'
|
|
66
|
+
email:
|
|
67
|
+
- michal.matyas@helpling.com
|
|
68
|
+
executables: []
|
|
69
|
+
extensions: []
|
|
70
|
+
extra_rdoc_files: []
|
|
71
|
+
files:
|
|
72
|
+
- CHANGELOG.md
|
|
73
|
+
- LICENSE.txt
|
|
74
|
+
- README.md
|
|
75
|
+
- lib/tty-command-window.rb
|
|
76
|
+
- lib/tty/command/window.rb
|
|
77
|
+
- lib/tty/command/window/ansi.rb
|
|
78
|
+
- lib/tty/command/window/block.rb
|
|
79
|
+
- lib/tty/command/window/child_session.rb
|
|
80
|
+
- lib/tty/command/window/coordinator.rb
|
|
81
|
+
- lib/tty/command/window/emulator.rb
|
|
82
|
+
- lib/tty/command/window/input_router.rb
|
|
83
|
+
- lib/tty/command/window/integration.rb
|
|
84
|
+
- lib/tty/command/window/runner.rb
|
|
85
|
+
- lib/tty/command/window/spinner.rb
|
|
86
|
+
- lib/tty/command/window/trap_manager.rb
|
|
87
|
+
- lib/tty/command/window/version.rb
|
|
88
|
+
- lib/tty/command/window/window_options.rb
|
|
89
|
+
homepage: https://github.com/helpling/tty-command-window
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
homepage_uri: https://github.com/helpling/tty-command-window
|
|
94
|
+
source_code_uri: https://github.com/helpling/tty-command-window
|
|
95
|
+
changelog_uri: https://github.com/helpling/tty-command-window/blob/main/CHANGELOG.md
|
|
96
|
+
rubygems_mfa_required: 'true'
|
|
97
|
+
post_install_message:
|
|
98
|
+
rdoc_options: []
|
|
99
|
+
require_paths:
|
|
100
|
+
- lib
|
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: 3.1.0
|
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
requirements: []
|
|
112
|
+
rubygems_version: 3.4.10
|
|
113
|
+
signing_key:
|
|
114
|
+
specification_version: 4
|
|
115
|
+
summary: Run commands with tty-command inside a live, fixed-height terminal window
|
|
116
|
+
test_files: []
|