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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +191 -19
  4. data/assets/keymaps/emacs.jsonc +5 -0
  5. data/assets/keymaps/jetbrains.jsonc +5 -0
  6. data/assets/keymaps/sublime.jsonc +5 -0
  7. data/assets/keymaps/vscode.jsonc +5 -0
  8. data/docs/debugging.md +143 -0
  9. data/docs/distribution.md +40 -9
  10. data/docs/lsp.md +131 -0
  11. data/docs/performance.md +18 -0
  12. data/docs/providers.md +36 -0
  13. data/docs/release-log.md +12 -0
  14. data/docs/tasks.md +64 -0
  15. data/docs/testing.md +49 -0
  16. data/docs/vim.md +3 -0
  17. data/exe/canopus +4 -1
  18. data/lib/canopus/buffer.rb +291 -50
  19. data/lib/canopus/cli.rb +5 -2
  20. data/lib/canopus/command.rb +13 -4
  21. data/lib/canopus/controller.rb +183 -40
  22. data/lib/canopus/debug/breakpoints.rb +550 -0
  23. data/lib/canopus/debug/configuration.rb +291 -0
  24. data/lib/canopus/debug/console.rb +243 -0
  25. data/lib/canopus/debug/panel.rb +487 -0
  26. data/lib/canopus/debug/session.rb +354 -0
  27. data/lib/canopus/decoration.rb +4 -2
  28. data/lib/canopus/denebola_compat.rb +27 -0
  29. data/lib/canopus/diagnostics.rb +173 -0
  30. data/lib/canopus/display_map/overlay_map.rb +6 -2
  31. data/lib/canopus/editor/snippet_expandable.rb +3 -2
  32. data/lib/canopus/editor.rb +122 -31
  33. data/lib/canopus/git/state.rb +100 -0
  34. data/lib/canopus/language/background_analysis.rb +93 -14
  35. data/lib/canopus/language/document.rb +70 -34
  36. data/lib/canopus/language/symbol.rb +1 -0
  37. data/lib/canopus/language/syntax_worker.rb +61 -9
  38. data/lib/canopus/language.rb +3 -0
  39. data/lib/canopus/minimap.rb +201 -0
  40. data/lib/canopus/patch.rb +3 -1
  41. data/lib/canopus/plugins/api.rb +4 -2
  42. data/lib/canopus/plugins/isolated_runtime.rb +64 -7
  43. data/lib/canopus/plugins/registry.rb +114 -3
  44. data/lib/canopus/plugins.rb +8 -7
  45. data/lib/canopus/project.rb +15 -6
  46. data/lib/canopus/provider.rb +192 -0
  47. data/lib/canopus/regexp_compat.rb +36 -4
  48. data/lib/canopus/selection.rb +12 -0
  49. data/lib/canopus/settings/schema.rb +201 -0
  50. data/lib/canopus/settings.rb +328 -49
  51. data/lib/canopus/task/configuration.rb +356 -0
  52. data/lib/canopus/task/problem_matcher.rb +264 -0
  53. data/lib/canopus/task/runner.rb +314 -0
  54. data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
  55. data/lib/canopus/test_runner/discovery.rb +97 -0
  56. data/lib/canopus/test_runner/execution.rb +194 -0
  57. data/lib/canopus/test_runner/minitest.rb +116 -0
  58. data/lib/canopus/test_runner/rspec.rb +103 -0
  59. data/lib/canopus/test_runner/safe_walker.rb +37 -0
  60. data/lib/canopus/test_runner.rb +54 -0
  61. data/lib/canopus/theme/import.rb +131 -0
  62. data/lib/canopus/theme.rb +28 -4
  63. data/lib/canopus/version.rb +1 -1
  64. data/lib/canopus/vim/motionable.rb +1 -1
  65. data/lib/canopus/vim.rb +10 -16
  66. data/lib/canopus/workspace/auto_savable.rb +83 -0
  67. data/lib/canopus/workspace/debug_aware.rb +600 -0
  68. data/lib/canopus/workspace/edit/plan.rb +2 -2
  69. data/lib/canopus/workspace/editor_configurable.rb +113 -0
  70. data/lib/canopus/workspace/file_change_aware.rb +11 -3
  71. data/lib/canopus/workspace/git_aware.rb +80 -34
  72. data/lib/canopus/workspace/git_blame.rb +175 -0
  73. data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
  74. data/lib/canopus/workspace/git_diff_view.rb +641 -0
  75. data/lib/canopus/workspace/git_history.rb +171 -0
  76. data/lib/canopus/workspace/git_remote.rb +267 -0
  77. data/lib/canopus/workspace/git_staging.rb +583 -0
  78. data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
  79. data/lib/canopus/workspace/keymap_aware.rb +66 -0
  80. data/lib/canopus/workspace/language_aware.rb +3721 -135
  81. data/lib/canopus/workspace/language_server_configurable.rb +287 -71
  82. data/lib/canopus/workspace/persistent_undo.rb +169 -0
  83. data/lib/canopus/workspace/problems_aware.rb +131 -0
  84. data/lib/canopus/workspace/project_searchable.rb +88 -11
  85. data/lib/canopus/workspace/project_tree_editable.rb +3 -1
  86. data/lib/canopus/workspace/recoverable.rb +204 -0
  87. data/lib/canopus/workspace/session_persistable.rb +85 -13
  88. data/lib/canopus/workspace/settings_aware.rb +76 -2
  89. data/lib/canopus/workspace/task_aware.rb +194 -0
  90. data/lib/canopus/workspace/test_aware.rb +394 -0
  91. data/lib/canopus/workspace/trust.rb +73 -0
  92. data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
  93. data/lib/canopus/workspace/view.rb +399 -74
  94. data/lib/canopus/workspace.rb +688 -44
  95. data/lib/canopus.rb +20 -2
  96. data/sig/canopus.rbs +138 -31
  97. data/sig/controller.rbs +10 -3
  98. data/sig/debug.rbs +121 -0
  99. data/sig/decoration.rbs +12 -2
  100. data/sig/diagnostics.rbs +26 -0
  101. data/sig/minimap.rbs +20 -0
  102. data/sig/provider.rbs +29 -0
  103. data/sig/services.rbs +1 -16
  104. data/sig/task.rbs +92 -0
  105. data/sig/test_runner.rbs +109 -0
  106. data/sig/trust.rbs +17 -0
  107. data/sig/workspace_edits.rbs +2 -2
  108. data/sig/workspace_services.rbs +234 -2
  109. data/tools/check_dependencies.rb +21 -2
  110. data/tools/package.rb +172 -14
  111. data/tools/package_signature_test.rb +33 -0
  112. data/tools/package_test.rb +29 -0
  113. data/tools/sign_package.rb +123 -0
  114. data/tools/update.rb +189 -0
  115. data/tools/update_test.rb +71 -0
  116. metadata +206 -31
  117. data/lib/canopus/lsp/client.rb +0 -307
  118. data/lib/canopus/lsp/error.rb +0 -7
  119. data/lib/canopus/lsp/future/subscription.rb +0 -9
  120. data/lib/canopus/lsp/future.rb +0 -87
  121. data/lib/canopus/lsp/protocol.rb +0 -83
  122. data/lib/canopus/lsp/server_error.rb +0 -13
  123. data/lib/canopus/lsp/timeout.rb +0 -7
  124. data/lib/canopus/lsp/transport.rb +0 -123
  125. data/lib/canopus/lsp.rb +0 -19
  126. data/lib/canopus/project/search.rb +0 -164
  127. data/lib/canopus/project/search_worker/cancelled.rb +0 -3
  128. data/lib/canopus/project/search_worker/runner.rb +0 -9
  129. data/lib/canopus/project/search_worker.rb +0 -108
  130. data/lib/canopus/terminal/cell.rb +0 -7
  131. data/lib/canopus/terminal/grid.rb +0 -321
  132. data/lib/canopus/terminal/pty.rb +0 -178
  133. data/lib/canopus/terminal/scrollback.rb +0 -38
  134. data/lib/canopus/terminal/vt.rb +0 -398
  135. data/lib/canopus/terminal.rb +0 -13
  136. data/sig/lsp.rbs +0 -99
  137. data/sig/terminal.rbs +0 -130
@@ -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
@@ -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
@@ -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