canopus 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
data/lib/canopus/terminal/pty.rb
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "io/console"
|
|
4
|
-
require "shellwords"
|
|
5
|
-
|
|
6
|
-
module Canopus
|
|
7
|
-
module Terminal
|
|
8
|
-
class PTY
|
|
9
|
-
attr_reader :reader, :writer, :pid, :grid, :vt, :status, :initial_cwd, :command_name
|
|
10
|
-
|
|
11
|
-
def initialize(command: ENV.fetch("SHELL", "/bin/sh"), cwd: Dir.pwd, columns: 80, rows: 24, env: {}, scrollback: 10_000,
|
|
12
|
-
queue_limit_bytes: 8_388_608)
|
|
13
|
-
raise ArgumentError, "terminal queue limit must be a positive integer" unless queue_limit_bytes.is_a?(Integer) && queue_limit_bytes.positive?
|
|
14
|
-
@initial_cwd = File.expand_path(cwd)
|
|
15
|
-
@command_name = File.basename(Array(command).first.to_s)
|
|
16
|
-
@grid = Grid.new(columns: columns, rows: rows, scrollback: scrollback)
|
|
17
|
-
if RUBY_PLATFORM.match?(/mswin|mingw/)
|
|
18
|
-
require "zaniah/platform/windows/terminal"
|
|
19
|
-
command = nil if command == "/bin/sh"
|
|
20
|
-
@native = Zaniah::Platform::Windows::Terminal.new(command: command, cwd: cwd, columns: columns, rows: rows, env: env)
|
|
21
|
-
@pid = @native.pid
|
|
22
|
-
@vt = VT.new(grid) { |bytes| write(bytes) }
|
|
23
|
-
return
|
|
24
|
-
end
|
|
25
|
-
require "pty"
|
|
26
|
-
arguments = command.is_a?(Array) ? command : Shellwords.split(command)
|
|
27
|
-
raise ArgumentError, "terminal command required" if arguments.empty?
|
|
28
|
-
@reader, @writer, @pid = ::PTY.spawn({"TERM" => "xterm-256color", "COLORTERM" => "truecolor"}.merge(env), *arguments, chdir: cwd)
|
|
29
|
-
@reader.binmode
|
|
30
|
-
@writer.binmode
|
|
31
|
-
@writer.sync = true
|
|
32
|
-
@vt = VT.new(grid) { |bytes| write(bytes) }
|
|
33
|
-
resize(columns: columns, rows: rows)
|
|
34
|
-
@queue, @queue_bytes, @queue_limit_bytes = [], 0, queue_limit_bytes
|
|
35
|
-
@queue_lock, @queue_ready = Mutex.new, ConditionVariable.new
|
|
36
|
-
start_reader
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# nil means EOF; an empty String means that no data was ready yet.
|
|
40
|
-
def read(timeout: 0, max_bytes: 65_536, max_seconds: nil)
|
|
41
|
-
raise ArgumentError, "terminal read limit must be a positive integer" unless max_bytes.is_a?(Integer) && max_bytes.positive?
|
|
42
|
-
raise ArgumentError, "terminal read timeout must be nonnegative" unless timeout.is_a?(Numeric) && timeout >= 0
|
|
43
|
-
raise ArgumentError, "terminal parse budget must be nonnegative" unless max_seconds.nil? || max_seconds.is_a?(Numeric) && max_seconds >= 0
|
|
44
|
-
return nil if @eof
|
|
45
|
-
if @native
|
|
46
|
-
data = @native.read_available(limit: max_bytes)
|
|
47
|
-
data ? vt.feed(data) : @eof = true
|
|
48
|
-
return data
|
|
49
|
-
end
|
|
50
|
-
deadline = max_seconds && Process.clock_gettime(Process::CLOCK_MONOTONIC) + [max_seconds, 0].max
|
|
51
|
-
data = +"".b
|
|
52
|
-
loop do
|
|
53
|
-
break if data.bytesize >= max_bytes || deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
54
|
-
piece = @queue_lock.synchronize do
|
|
55
|
-
@queue_ready.wait(@queue_lock, timeout) if data.empty? && @queue.empty? && !@reader_eof && timeout.positive?
|
|
56
|
-
if @queue.empty?
|
|
57
|
-
@eof = true if @reader_eof
|
|
58
|
-
nil
|
|
59
|
-
else
|
|
60
|
-
chunk = @queue.shift
|
|
61
|
-
take = [max_bytes - data.bytesize, chunk.bytesize, 16_384].min
|
|
62
|
-
result = chunk.byteslice(0, take)
|
|
63
|
-
@queue.unshift(chunk.byteslice(take..)) if take < chunk.bytesize
|
|
64
|
-
@queue_bytes -= take
|
|
65
|
-
@queue_ready.broadcast
|
|
66
|
-
result
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
break unless piece
|
|
70
|
-
vt.feed(piece)
|
|
71
|
-
data << piece
|
|
72
|
-
end
|
|
73
|
-
@eof && data.empty? ? nil : data
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def pending? = @native ? false : @queue_lock.synchronize { !@queue.empty? }
|
|
77
|
-
|
|
78
|
-
def write(bytes) = @native ? @native.write(bytes) : writer.write(bytes)
|
|
79
|
-
def paste(text) = write(vt.paste(text))
|
|
80
|
-
def key(name, **modifiers) = write(vt.key(name, **modifiers))
|
|
81
|
-
def mouse(**event) = write(vt.mouse(**event))
|
|
82
|
-
|
|
83
|
-
def resize(columns:, rows:)
|
|
84
|
-
dimensions = [columns, rows]
|
|
85
|
-
return self if @pty_size == dimensions
|
|
86
|
-
grid.resize(columns: columns, rows: rows) unless grid.columns == columns && grid.rows == rows
|
|
87
|
-
@native ? @native.resize(columns, rows) : reader.winsize = [rows, columns]
|
|
88
|
-
@pty_size = dimensions
|
|
89
|
-
self
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def busy?
|
|
93
|
-
!@native && alive? && reader.tcgetpgrp != pid
|
|
94
|
-
rescue IOError, SystemCallError, NoMethodError
|
|
95
|
-
false
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def foreground_process_name(now: Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
99
|
-
return @foreground_name if @foreground_checked && now - @foreground_checked < 1
|
|
100
|
-
@foreground_checked = now
|
|
101
|
-
group = reader.tcgetpgrp unless @native
|
|
102
|
-
value = if group && group != pid && File.file?("/proc/#{group}/comm")
|
|
103
|
-
File.read("/proc/#{group}/comm")
|
|
104
|
-
elsif group && group != pid
|
|
105
|
-
IO.popen(["ps", "-o", "comm=", "-p", group.to_s], &:read)
|
|
106
|
-
end
|
|
107
|
-
@foreground_name = value&.strip&.then { |name| File.basename(name) unless name.empty? }
|
|
108
|
-
rescue IOError, SystemCallError, NoMethodError
|
|
109
|
-
@foreground_name = nil
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def signal(name = "INT")
|
|
113
|
-
return @native.write("\x03") if @native && name == "INT"
|
|
114
|
-
return @native.close if @native
|
|
115
|
-
Process.kill(name, -pid)
|
|
116
|
-
rescue Errno::ESRCH
|
|
117
|
-
false
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def alive?
|
|
121
|
-
return @native.alive? if @native
|
|
122
|
-
return false if @status
|
|
123
|
-
result = Process.waitpid2(pid, Process::WNOHANG)
|
|
124
|
-
@status = result.last if result
|
|
125
|
-
!result
|
|
126
|
-
rescue Errno::ECHILD
|
|
127
|
-
false
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def close
|
|
131
|
-
return @native.close if @native
|
|
132
|
-
@closing = true
|
|
133
|
-
@queue_lock.synchronize { @queue_ready.broadcast }
|
|
134
|
-
signal("HUP") if alive?
|
|
135
|
-
reader.close unless reader.closed?
|
|
136
|
-
writer.close unless writer.closed?
|
|
137
|
-
# A shell may trap HUP: reap it after a short grace period.
|
|
138
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
|
|
139
|
-
while alive? && Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
140
|
-
sleep 0.01
|
|
141
|
-
end
|
|
142
|
-
if alive?
|
|
143
|
-
signal("KILL")
|
|
144
|
-
_, @status = Process.waitpid2(pid)
|
|
145
|
-
end
|
|
146
|
-
@reader_thread&.join(0.2)
|
|
147
|
-
@eof = true
|
|
148
|
-
self
|
|
149
|
-
rescue Errno::ECHILD, IOError
|
|
150
|
-
self
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
def start_reader
|
|
156
|
-
@reader_thread = Thread.new do
|
|
157
|
-
loop do
|
|
158
|
-
data = reader.readpartial(65_536)
|
|
159
|
-
@queue_lock.synchronize do
|
|
160
|
-
@queue_ready.wait(@queue_lock) while !@closing && @queue_bytes >= @queue_limit_bytes
|
|
161
|
-
break if @closing
|
|
162
|
-
@queue << data
|
|
163
|
-
@queue_bytes += data.bytesize
|
|
164
|
-
@queue_ready.broadcast
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
rescue Errno::EIO, EOFError, IOError
|
|
168
|
-
nil
|
|
169
|
-
ensure
|
|
170
|
-
@queue_lock.synchronize do
|
|
171
|
-
@reader_eof = true
|
|
172
|
-
@queue_ready.broadcast
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "denebola"
|
|
4
|
-
|
|
5
|
-
module Canopus
|
|
6
|
-
module Terminal
|
|
7
|
-
class Scrollback
|
|
8
|
-
include Enumerable
|
|
9
|
-
Count = Struct.new(:lines) do
|
|
10
|
-
def self.zero = new(0)
|
|
11
|
-
def +(other) = self.class.new(lines + other.lines)
|
|
12
|
-
end
|
|
13
|
-
Row = Struct.new(:cells) do
|
|
14
|
-
def summary = Count.new(1)
|
|
15
|
-
end
|
|
16
|
-
attr_reader :tree, :limit
|
|
17
|
-
|
|
18
|
-
def initialize(limit)
|
|
19
|
-
raise ArgumentError, "scrollback limit must be nonnegative" unless limit.is_a?(Integer) && limit >= 0
|
|
20
|
-
@limit = limit
|
|
21
|
-
clear
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def clear = @tree = Denebola::Tree.new(summary: Count)
|
|
25
|
-
def length = tree.size
|
|
26
|
-
alias size length
|
|
27
|
-
def [](index) = tree[index]&.cells
|
|
28
|
-
def each(&block) = block ? tree.each { |row| block.call(row.cells) } : enum_for(__method__)
|
|
29
|
-
|
|
30
|
-
def push(cells)
|
|
31
|
-
return if limit.zero?
|
|
32
|
-
frozen = cells.map { |cell| cell.dup.tap { |copy| copy.text = copy.text.dup.freeze }.freeze }.freeze
|
|
33
|
-
@tree = tree.push(Row.new(frozen).freeze)
|
|
34
|
-
@tree = tree.slice(tree.size - limit, limit) if tree.size > limit
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
data/lib/canopus/terminal/vt.rb
DELETED
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "uri"
|
|
4
|
-
|
|
5
|
-
module Canopus
|
|
6
|
-
module Terminal
|
|
7
|
-
class VT
|
|
8
|
-
DEC_GRAPHICS = {"`" => "◆", "a" => "▒", "f" => "°", "g" => "±", "j" => "┘", "k" => "┐",
|
|
9
|
-
"l" => "┌", "m" => "└", "n" => "┼", "o" => "⎺", "p" => "⎻", "q" => "─", "r" => "⎼", "s" => "⎽",
|
|
10
|
-
"t" => "├", "u" => "┤", "v" => "┴", "w" => "┬", "x" => "│", "y" => "≤", "z" => "≥", "{" => "π", "|" => "≠", "}" => "£", "~" => "·"}.freeze
|
|
11
|
-
attr_reader :grid, :title, :cwd, :modes, :replies, :bell_count
|
|
12
|
-
|
|
13
|
-
def initialize(grid = Grid.new, &reply)
|
|
14
|
-
@grid = grid
|
|
15
|
-
@reply = reply
|
|
16
|
-
@replies = []
|
|
17
|
-
@input = +"".b
|
|
18
|
-
@state = :ground
|
|
19
|
-
@sequence = +"".b
|
|
20
|
-
@modes = {7 => true, 25 => true}
|
|
21
|
-
@charsets = [:ascii, :ascii]
|
|
22
|
-
@charset = 0
|
|
23
|
-
@bell_count = 0
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# The parser retains incomplete UTF-8 and control sequences between reads.
|
|
27
|
-
def feed(bytes)
|
|
28
|
-
@input << bytes.b
|
|
29
|
-
index = 0
|
|
30
|
-
while index < @input.bytesize
|
|
31
|
-
byte = @input.getbyte(index)
|
|
32
|
-
if @state == :ground && byte >= 0xa0
|
|
33
|
-
length = if byte.between?(0xc2, 0xdf) then 2
|
|
34
|
-
elsif byte.between?(0xe0, 0xef) then 3
|
|
35
|
-
elsif byte.between?(0xf0, 0xf4) then 4
|
|
36
|
-
else 1 end
|
|
37
|
-
break if index + length > @input.bytesize
|
|
38
|
-
char = @input.byteslice(index, length).force_encoding(Encoding::UTF_8)
|
|
39
|
-
unless char.valid_encoding?
|
|
40
|
-
char = "\ufffd"
|
|
41
|
-
length = 1
|
|
42
|
-
end
|
|
43
|
-
print(char)
|
|
44
|
-
index += length
|
|
45
|
-
next
|
|
46
|
-
end
|
|
47
|
-
consume(byte)
|
|
48
|
-
index += 1
|
|
49
|
-
end
|
|
50
|
-
@input = @input.byteslice(index..) || +"".b
|
|
51
|
-
self
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def paste(text)
|
|
55
|
-
modes[2004] ? "\e[200~#{text.gsub("\e[201~", "")}\e[201~" : text
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def mouse(button:, column:, row:, action: :press, shift: false, alt: false, control: false)
|
|
59
|
-
tracking = [1000, 1002, 1003].find { |mode| modes[mode] }
|
|
60
|
-
return "" unless tracking
|
|
61
|
-
return "" if action == :move && (tracking == 1000 || (tracking == 1002 && button.nil?))
|
|
62
|
-
code = case button
|
|
63
|
-
when :left, 0 then 0
|
|
64
|
-
when :middle, 1 then 1
|
|
65
|
-
when :right, 2 then 2
|
|
66
|
-
when :wheel_up then 64
|
|
67
|
-
when :wheel_down then 65
|
|
68
|
-
else 3 end
|
|
69
|
-
code |= 32 if action == :move
|
|
70
|
-
code |= 4 if shift
|
|
71
|
-
code |= 8 if alt
|
|
72
|
-
code |= 16 if control
|
|
73
|
-
column = [column + 1, 1].max
|
|
74
|
-
row = [row + 1, 1].max
|
|
75
|
-
if modes[1006]
|
|
76
|
-
"\e[<#{code};#{column};#{row}#{action == :release ? 'm' : 'M'}"
|
|
77
|
-
else
|
|
78
|
-
code = (code & ~3) | 3 if action == :release
|
|
79
|
-
return "" if column > 223 || row > 223
|
|
80
|
-
"\e[M".b + [code + 32, column + 32, row + 32].pack("C3")
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def key(name, shift: false, alt: false, control: false)
|
|
85
|
-
name = name.to_s
|
|
86
|
-
modifier = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (control ? 4 : 0)
|
|
87
|
-
arrows = {"up" => "A", "down" => "B", "right" => "C", "left" => "D", "home" => "H", "end" => "F"}
|
|
88
|
-
if arrows.key?(name)
|
|
89
|
-
return modifier > 1 ? "\e[1;#{modifier}#{arrows[name]}" : "\e#{modes[1] ? 'O' : '['}#{arrows[name]}"
|
|
90
|
-
end
|
|
91
|
-
function = {"insert" => 2, "delete" => 3, "page_up" => 5, "page_down" => 6,
|
|
92
|
-
"f5" => 15, "f6" => 17, "f7" => 18, "f8" => 19, "f9" => 20, "f10" => 21, "f11" => 23, "f12" => 24}
|
|
93
|
-
return "\e[#{function[name]}#{modifier > 1 ? ";#{modifier}" : ''}~" if function.key?(name)
|
|
94
|
-
if /\Af[1-4]\z/.match?(name)
|
|
95
|
-
final = ("P".ord + name[1].to_i - 1).chr
|
|
96
|
-
return modifier > 1 ? "\e[1;#{modifier}#{final}" : "\eO#{final}"
|
|
97
|
-
end
|
|
98
|
-
return "\e[Z" if name == "tab" && shift
|
|
99
|
-
text = {"enter" => "\r", "backspace" => "\x7f", "tab" => "\t", "escape" => "\e"}.fetch(name, name)
|
|
100
|
-
text = (text.upcase.ord & 0x1f).chr if control && text.length == 1 && ("@".."_").cover?(text.upcase)
|
|
101
|
-
text = "\0" if control && text == " "
|
|
102
|
-
alt ? "\e#{text}" : text
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
private
|
|
106
|
-
|
|
107
|
-
def consume(byte)
|
|
108
|
-
if [:osc, :dcs, :string].include?(@state)
|
|
109
|
-
return consume_string(byte)
|
|
110
|
-
end
|
|
111
|
-
if byte == 0x1b
|
|
112
|
-
@state = :escape
|
|
113
|
-
@sequence.clear
|
|
114
|
-
return
|
|
115
|
-
elsif [0x18, 0x1a].include?(byte)
|
|
116
|
-
@state = :ground
|
|
117
|
-
return
|
|
118
|
-
elsif byte < 0x20 || byte == 0x7f
|
|
119
|
-
control(byte)
|
|
120
|
-
return
|
|
121
|
-
end
|
|
122
|
-
case @state
|
|
123
|
-
when :ground
|
|
124
|
-
case byte
|
|
125
|
-
when 0x9b then start_sequence(:csi)
|
|
126
|
-
when 0x9d then start_sequence(:osc)
|
|
127
|
-
when 0x90 then start_sequence(:dcs)
|
|
128
|
-
when 0x84 then grid.linefeed
|
|
129
|
-
when 0x85 then grid.carriage_return; grid.linefeed
|
|
130
|
-
when 0x8d then grid.reverse_index
|
|
131
|
-
else print(byte.chr) if byte < 0x80
|
|
132
|
-
end
|
|
133
|
-
when :escape
|
|
134
|
-
case byte.chr
|
|
135
|
-
when "[" then start_sequence(:csi)
|
|
136
|
-
when "]" then start_sequence(:osc)
|
|
137
|
-
when "P" then start_sequence(:dcs)
|
|
138
|
-
when "_", "^", "X" then start_sequence(:string)
|
|
139
|
-
when "(" then @charset_target = 0; @state = :charset
|
|
140
|
-
when ")" then @charset_target = 1; @state = :charset
|
|
141
|
-
when "#", "%" then @state = :escape_intermediate; @sequence << byte
|
|
142
|
-
else
|
|
143
|
-
escape(byte.chr)
|
|
144
|
-
@state = :ground
|
|
145
|
-
end
|
|
146
|
-
when :escape_intermediate
|
|
147
|
-
if @sequence == "#" && byte == "8".ord
|
|
148
|
-
grid.cells.each { |row| row.each { |cell| cell.text = "E"; cell.width = 1 } }
|
|
149
|
-
end
|
|
150
|
-
@state = :ground
|
|
151
|
-
when :charset
|
|
152
|
-
@charsets[@charset_target] = byte == "0".ord ? :graphics : :ascii
|
|
153
|
-
@state = :ground
|
|
154
|
-
when :csi
|
|
155
|
-
if byte.between?(0x40, 0x7e)
|
|
156
|
-
csi(byte.chr, @sequence)
|
|
157
|
-
@state = :ground
|
|
158
|
-
elsif @sequence.bytesize < 1024 && byte.between?(0x20, 0x3f)
|
|
159
|
-
@sequence << byte
|
|
160
|
-
else
|
|
161
|
-
@state = :csi_ignore
|
|
162
|
-
end
|
|
163
|
-
when :csi_ignore
|
|
164
|
-
@state = :ground if byte.between?(0x40, 0x7e)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def start_sequence(state)
|
|
169
|
-
@state = state
|
|
170
|
-
@sequence = +"".b
|
|
171
|
-
@string_escape = @string_overflow = false
|
|
172
|
-
@string_utf8_remaining = 0
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def consume_string(byte)
|
|
176
|
-
if @string_utf8_remaining.positive? && byte.between?(0x80, 0xbf)
|
|
177
|
-
@string_utf8_remaining -= 1
|
|
178
|
-
append_string(byte)
|
|
179
|
-
return
|
|
180
|
-
end
|
|
181
|
-
@string_utf8_remaining = if byte.between?(0xc2, 0xdf) then 1
|
|
182
|
-
elsif byte.between?(0xe0, 0xef) then 2
|
|
183
|
-
elsif byte.between?(0xf0, 0xf4) then 3
|
|
184
|
-
else 0 end
|
|
185
|
-
if byte == 0x9c || (@string_escape && byte == 0x5c) || (byte == 7 && @state == :osc)
|
|
186
|
-
if !@string_overflow
|
|
187
|
-
osc(@sequence) if @state == :osc
|
|
188
|
-
dcs(@sequence) if @state == :dcs
|
|
189
|
-
end
|
|
190
|
-
@state = :ground
|
|
191
|
-
@string_escape = false
|
|
192
|
-
elsif [0x18, 0x1a].include?(byte)
|
|
193
|
-
@state = :ground
|
|
194
|
-
elsif byte == 0x1b
|
|
195
|
-
@string_escape = true
|
|
196
|
-
else
|
|
197
|
-
@sequence << 0x1b if @string_escape && !@string_overflow
|
|
198
|
-
@string_escape = false
|
|
199
|
-
append_string(byte)
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def append_string(byte)
|
|
204
|
-
@sequence.bytesize < 16_384 ? @sequence << byte : @string_overflow = true
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def control(byte)
|
|
208
|
-
case byte
|
|
209
|
-
when 7 then @bell_count += 1
|
|
210
|
-
when 8 then grid.backspace
|
|
211
|
-
when 9 then grid.tab
|
|
212
|
-
when 10, 11, 12 then grid.linefeed; grid.carriage_return if modes[20]
|
|
213
|
-
when 13 then grid.carriage_return
|
|
214
|
-
when 14 then @charset = 1
|
|
215
|
-
when 15 then @charset = 0
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def escape(char)
|
|
220
|
-
case char
|
|
221
|
-
when "7" then grid.save_cursor
|
|
222
|
-
when "8" then grid.restore_cursor
|
|
223
|
-
when "D" then grid.linefeed
|
|
224
|
-
when "E" then grid.carriage_return; grid.linefeed
|
|
225
|
-
when "M" then grid.reverse_index
|
|
226
|
-
when "H" then grid.tab_set
|
|
227
|
-
when "=" then @modes[:keypad] = true
|
|
228
|
-
when ">" then @modes[:keypad] = false
|
|
229
|
-
when "c" then grid.reset; @modes = {7 => true, 25 => true}; @charsets = [:ascii, :ascii]; @charset = 0
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def print(char)
|
|
234
|
-
char = DEC_GRAPHICS.fetch(char, char) if @charsets[@charset] == :graphics
|
|
235
|
-
grid.put(char)
|
|
236
|
-
@last_printed = char
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def csi(final, sequence)
|
|
240
|
-
prefix = sequence[/\A[?<=>]/]
|
|
241
|
-
parameters = sequence.sub(/\A[?<=>]/, "").sub(/[\x20-\x2f]+\z/, "")
|
|
242
|
-
values = parameters.split(";", -1).map { |value| value.empty? ? 0 : value.to_i.clamp(0, 1_000_000) }
|
|
243
|
-
values = [0] if values.empty?
|
|
244
|
-
amount = values.first.zero? ? 1 : values.first
|
|
245
|
-
case final
|
|
246
|
-
when "A" then grid.move(y: grid.cursor_y - amount)
|
|
247
|
-
when "B", "e" then grid.move(y: grid.cursor_y + amount)
|
|
248
|
-
when "C", "a" then grid.move(x: grid.cursor_x + amount)
|
|
249
|
-
when "D" then grid.move(x: grid.cursor_x - amount)
|
|
250
|
-
when "E" then grid.move(x: 0, y: grid.cursor_y + amount)
|
|
251
|
-
when "F" then grid.move(x: 0, y: grid.cursor_y - amount)
|
|
252
|
-
when "G", "`" then grid.move(x: amount - 1)
|
|
253
|
-
when "H", "f" then grid.position(amount, values[1].to_i.zero? ? 1 : values[1])
|
|
254
|
-
when "d" then grid.position(amount, grid.cursor_x + 1)
|
|
255
|
-
when "I" then grid.tab([amount, grid.columns].min)
|
|
256
|
-
when "Z" then grid.tab([amount, grid.columns].min, backward: true)
|
|
257
|
-
when "J" then grid.erase_display(values.first)
|
|
258
|
-
when "K" then grid.erase_line(values.first)
|
|
259
|
-
when "L" then grid.insert_lines(amount)
|
|
260
|
-
when "M" then grid.delete_lines(amount)
|
|
261
|
-
when "P" then grid.delete_characters(amount)
|
|
262
|
-
when "@" then grid.insert_characters(amount)
|
|
263
|
-
when "X" then grid.erase_characters(amount)
|
|
264
|
-
when "S" then grid.scroll_up(amount)
|
|
265
|
-
when "T" then grid.scroll_down(amount) unless values.length > 1
|
|
266
|
-
when "r" then grid.margins(amount, values[1].to_i.zero? ? grid.rows : values[1]) unless prefix
|
|
267
|
-
when "s" then grid.save_cursor
|
|
268
|
-
when "u" then grid.restore_cursor
|
|
269
|
-
when "g" then grid.tab_clear(all: values.first == 3)
|
|
270
|
-
when "m" then sgr(parameters) unless prefix
|
|
271
|
-
when "h", "l" then values.each { |mode| set_mode(mode, final == "h", prefix == "?") }
|
|
272
|
-
when "b" then [amount, grid.columns * grid.rows].min.times { grid.put(@last_printed) } if @last_printed
|
|
273
|
-
when "n"
|
|
274
|
-
emit("\e[0n") if values.first == 5
|
|
275
|
-
emit("\e[#{prefix == '?' ? '?' : ''}#{grid.cursor_y + 1};#{grid.cursor_x + 1}R") if values.first == 6
|
|
276
|
-
when "c"
|
|
277
|
-
emit(prefix == ">" ? "\e[>0;1;0c" : "\e[?1;2c")
|
|
278
|
-
when "t"
|
|
279
|
-
emit("\e[8;#{grid.rows};#{grid.columns}t") if values.first == 18
|
|
280
|
-
emit("\e]l#{title}\e\\") if values.first == 21
|
|
281
|
-
when "p"
|
|
282
|
-
if sequence.end_with?("!")
|
|
283
|
-
grid.attributes = {}.freeze
|
|
284
|
-
grid.foreground = grid.background = nil
|
|
285
|
-
grid.insert_mode = grid.origin_mode = false
|
|
286
|
-
grid.autowrap = grid.cursor_visible = true
|
|
287
|
-
grid.margins
|
|
288
|
-
elsif sequence.end_with?("$")
|
|
289
|
-
state = modes.key?(values.first) ? modes[values.first] ? 1 : 2 : 0
|
|
290
|
-
emit("\e[?#{values.first};#{state}$y")
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def set_mode(mode, enabled, private_mode)
|
|
296
|
-
@modes[mode] = enabled
|
|
297
|
-
if private_mode
|
|
298
|
-
case mode
|
|
299
|
-
when 6 then grid.origin_mode = enabled; grid.position(1, 1)
|
|
300
|
-
when 7 then grid.autowrap = enabled
|
|
301
|
-
when 25 then grid.cursor_visible = enabled
|
|
302
|
-
when 47, 1047 then grid.alternate(enabled, save: false)
|
|
303
|
-
when 1048 then enabled ? grid.save_cursor : grid.restore_cursor
|
|
304
|
-
when 1049 then grid.alternate(enabled)
|
|
305
|
-
when 1000, 1002, 1003
|
|
306
|
-
[1000, 1002, 1003].each { |other| @modes[other] = false unless other == mode } if enabled
|
|
307
|
-
end
|
|
308
|
-
elsif mode == 4
|
|
309
|
-
grid.insert_mode = enabled
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def sgr(parameters)
|
|
314
|
-
# Colon subparameters are retained for underline styles and ISO RGB.
|
|
315
|
-
groups = parameters.empty? ? ["0"] : parameters.split(";", -1)
|
|
316
|
-
attributes = grid.attributes.dup
|
|
317
|
-
index = 0
|
|
318
|
-
while index < groups.length
|
|
319
|
-
parts = groups[index].split(":", -1)
|
|
320
|
-
value = parts.first.to_i
|
|
321
|
-
case value
|
|
322
|
-
when 0 then attributes.clear; grid.foreground = grid.background = nil
|
|
323
|
-
when 1 then attributes[:bold] = true
|
|
324
|
-
when 2 then attributes[:dim] = true
|
|
325
|
-
when 3 then attributes[:italic] = true
|
|
326
|
-
when 4 then attributes[:underline] = parts[1] ? parts[1].to_i : 1
|
|
327
|
-
when 5, 6 then attributes[:blink] = true
|
|
328
|
-
when 7 then attributes[:inverse] = true
|
|
329
|
-
when 8 then attributes[:hidden] = true
|
|
330
|
-
when 9 then attributes[:strikethrough] = true
|
|
331
|
-
when 21 then attributes[:underline] = 2
|
|
332
|
-
when 22 then attributes.delete(:bold); attributes.delete(:dim)
|
|
333
|
-
when 23 then attributes.delete(:italic)
|
|
334
|
-
when 24 then attributes.delete(:underline)
|
|
335
|
-
when 25 then attributes.delete(:blink)
|
|
336
|
-
when 27 then attributes.delete(:inverse)
|
|
337
|
-
when 28 then attributes.delete(:hidden)
|
|
338
|
-
when 29 then attributes.delete(:strikethrough)
|
|
339
|
-
when 30..37 then grid.foreground = value - 30
|
|
340
|
-
when 40..47 then grid.background = value - 40
|
|
341
|
-
when 90..97 then grid.foreground = value - 90 + 8
|
|
342
|
-
when 100..107 then grid.background = value - 100 + 8
|
|
343
|
-
when 39 then grid.foreground = nil
|
|
344
|
-
when 49 then grid.background = nil
|
|
345
|
-
when 38, 48, 58
|
|
346
|
-
color = nil
|
|
347
|
-
if parts.length > 1
|
|
348
|
-
kind = parts[1].to_i
|
|
349
|
-
color = kind == 5 ? parts[2]&.to_i : kind == 2 && parts.length >= 5 ? parts.last(3).map(&:to_i) : nil
|
|
350
|
-
else
|
|
351
|
-
kind = groups[index + 1]&.to_i
|
|
352
|
-
count = kind == 5 ? 1 : kind == 2 ? 3 : 0
|
|
353
|
-
if count.positive? && index + 1 + count < groups.length
|
|
354
|
-
color = count == 1 ? groups[index + 2].to_i : groups[index + 2, count].map(&:to_i)
|
|
355
|
-
index += count + 1
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
color = color.map { |part| part.clamp(0, 255) }.freeze if color.is_a?(Array)
|
|
359
|
-
color = color.clamp(0, 255) if color.is_a?(Integer)
|
|
360
|
-
if color
|
|
361
|
-
value == 38 ? grid.foreground = color : value == 48 ? grid.background = color : attributes[:underline_color] = color
|
|
362
|
-
end
|
|
363
|
-
when 59 then attributes.delete(:underline_color)
|
|
364
|
-
end
|
|
365
|
-
index += 1
|
|
366
|
-
end
|
|
367
|
-
grid.attributes = attributes.freeze
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
def osc(sequence)
|
|
371
|
-
command, payload = sequence.force_encoding(Encoding::UTF_8).scrub.split(";", 2)
|
|
372
|
-
return unless payload
|
|
373
|
-
case command
|
|
374
|
-
when "0", "2" then @title = payload
|
|
375
|
-
when "7"
|
|
376
|
-
uri = URI.parse(payload)
|
|
377
|
-
@cwd = URI::RFC2396_PARSER.unescape(uri.path) if uri.scheme == "file" && uri.path&.start_with?("/")
|
|
378
|
-
when "8"
|
|
379
|
-
_, url = payload.split(";", 2)
|
|
380
|
-
grid.hyperlink = url.nil? || url.empty? ? nil : url.freeze
|
|
381
|
-
end
|
|
382
|
-
rescue URI::InvalidURIError
|
|
383
|
-
nil
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
def dcs(sequence)
|
|
387
|
-
return unless sequence.start_with?("$q")
|
|
388
|
-
request = sequence.delete_prefix("$q")
|
|
389
|
-
answer = {"m" => "0m", "r" => "#{grid.scroll_top + 1};#{grid.scroll_bottom + 1}r"}[request]
|
|
390
|
-
emit("\eP#{answer ? 1 : 0}$r#{answer || request}\e\\")
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def emit(bytes)
|
|
394
|
-
@reply ? @reply.call(bytes) : @replies << bytes
|
|
395
|
-
end
|
|
396
|
-
end
|
|
397
|
-
end
|
|
398
|
-
end
|
data/lib/canopus/terminal.rb
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "terminal/cell"
|
|
4
|
-
require_relative "terminal/scrollback"
|
|
5
|
-
require_relative "terminal/grid"
|
|
6
|
-
require_relative "terminal/vt"
|
|
7
|
-
require_relative "terminal/pty"
|
|
8
|
-
|
|
9
|
-
module Canopus
|
|
10
|
-
module Terminal
|
|
11
|
-
# Renderers consume Grid#cells; VT only mutates screen state and emits replies.
|
|
12
|
-
end
|
|
13
|
-
end
|