echoes 0.2.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/CLAUDE.md +33 -0
- data/Echoes.app/Contents/Info.plist +16 -0
- data/Echoes.app/Contents/MacOS/Echoes +50 -0
- data/EchoesEmbed.app/Contents/Info.plist +16 -0
- data/EchoesEmbed.app/Contents/MacOS/EchoesEmbed +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +45 -0
- data/exe/echoes +15 -0
- data/lib/echoes/cell.rb +54 -0
- data/lib/echoes/client.rb +96 -0
- data/lib/echoes/configuration.rb +135 -0
- data/lib/echoes/copy_mode.rb +545 -0
- data/lib/echoes/cursor.rb +18 -0
- data/lib/echoes/editor.rb +225 -0
- data/lib/echoes/embedded_shell.rb +360 -0
- data/lib/echoes/embedded_shell_helper.rb +265 -0
- data/lib/echoes/gui.rb +2861 -0
- data/lib/echoes/installer.rb +95 -0
- data/lib/echoes/objc.rb +188 -0
- data/lib/echoes/pane.rb +1122 -0
- data/lib/echoes/pane_tree.rb +194 -0
- data/lib/echoes/parser.rb +821 -0
- data/lib/echoes/preferences.rb +45 -0
- data/lib/echoes/screen.rb +1468 -0
- data/lib/echoes/sixel_decoder.rb +221 -0
- data/lib/echoes/tab.rb +152 -0
- data/lib/echoes/terminal.rb +124 -0
- data/lib/echoes/version.rb +5 -0
- data/lib/echoes.rb +37 -0
- data/sig/echoes.rbs +4 -0
- metadata +123 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Echoes
|
|
4
|
+
class SixelDecoder
|
|
5
|
+
attr_reader :width, :height
|
|
6
|
+
|
|
7
|
+
def initialize(params = [])
|
|
8
|
+
@background_mode = params[1] || 0
|
|
9
|
+
@color_registers = default_color_registers
|
|
10
|
+
@current_color = 0
|
|
11
|
+
@cursor_x = 0
|
|
12
|
+
@cursor_y = 0
|
|
13
|
+
@width = 0
|
|
14
|
+
@height = 0
|
|
15
|
+
@pixels = {}
|
|
16
|
+
@declared_width = 0
|
|
17
|
+
@declared_height = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def decode(data)
|
|
21
|
+
i = 0
|
|
22
|
+
len = data.bytesize
|
|
23
|
+
|
|
24
|
+
while i < len
|
|
25
|
+
byte = data.getbyte(i)
|
|
26
|
+
|
|
27
|
+
case byte
|
|
28
|
+
when 0x21 # '!' repeat
|
|
29
|
+
i, count, ch = parse_repeat(data, i + 1)
|
|
30
|
+
paint_sixel(ch, count) if ch
|
|
31
|
+
when 0x22 # '"' raster attributes
|
|
32
|
+
i, _, _, ph, pv = parse_raster_attributes(data, i + 1)
|
|
33
|
+
@declared_width = ph if ph > 0
|
|
34
|
+
@declared_height = pv if pv > 0
|
|
35
|
+
when 0x23 # '#' color
|
|
36
|
+
i = parse_color(data, i + 1)
|
|
37
|
+
when 0x24 # '$' graphics CR
|
|
38
|
+
@cursor_x = 0
|
|
39
|
+
i += 1
|
|
40
|
+
when 0x2D # '-' graphics newline
|
|
41
|
+
@cursor_x = 0
|
|
42
|
+
@cursor_y += 6
|
|
43
|
+
i += 1
|
|
44
|
+
when 0x3F..0x7E # sixel character
|
|
45
|
+
paint_sixel(byte, 1)
|
|
46
|
+
i += 1
|
|
47
|
+
else
|
|
48
|
+
i += 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@width = @declared_width if @declared_width > @width
|
|
53
|
+
@height = @declared_height if @declared_height > @height
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_rgba
|
|
58
|
+
buf = "\x00".b * (@width * @height * 4)
|
|
59
|
+
transparent = @background_mode == 1
|
|
60
|
+
|
|
61
|
+
@pixels.each do |(x, y), color|
|
|
62
|
+
next if x >= @width || y >= @height
|
|
63
|
+
|
|
64
|
+
offset = (y * @width + x) * 4
|
|
65
|
+
buf.setbyte(offset, color[0])
|
|
66
|
+
buf.setbyte(offset + 1, color[1])
|
|
67
|
+
buf.setbyte(offset + 2, color[2])
|
|
68
|
+
buf.setbyte(offset + 3, 255)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
unless transparent
|
|
72
|
+
# Set alpha=255 for unset pixels (background fill)
|
|
73
|
+
(@width * @height).times do |i|
|
|
74
|
+
offset = i * 4
|
|
75
|
+
buf.setbyte(offset + 3, 255) if buf.getbyte(offset + 3) == 0
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
buf
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def default_color_registers
|
|
85
|
+
{
|
|
86
|
+
0 => [0, 0, 0],
|
|
87
|
+
1 => [51, 51, 204],
|
|
88
|
+
2 => [204, 51, 51],
|
|
89
|
+
3 => [51, 204, 51],
|
|
90
|
+
4 => [204, 51, 204],
|
|
91
|
+
5 => [51, 204, 204],
|
|
92
|
+
6 => [204, 204, 51],
|
|
93
|
+
7 => [135, 135, 135],
|
|
94
|
+
8 => [68, 68, 68],
|
|
95
|
+
9 => [84, 84, 255],
|
|
96
|
+
10 => [255, 84, 84],
|
|
97
|
+
11 => [84, 255, 84],
|
|
98
|
+
12 => [255, 84, 255],
|
|
99
|
+
13 => [84, 255, 255],
|
|
100
|
+
14 => [255, 255, 84],
|
|
101
|
+
15 => [255, 255, 255],
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def paint_sixel(byte, count)
|
|
106
|
+
bits = byte - 0x3F
|
|
107
|
+
color = @color_registers[@current_color] || [0, 0, 0]
|
|
108
|
+
|
|
109
|
+
6.times do |bit|
|
|
110
|
+
next unless (bits >> bit) & 1 == 1
|
|
111
|
+
|
|
112
|
+
y = @cursor_y + bit
|
|
113
|
+
count.times do |dx|
|
|
114
|
+
x = @cursor_x + dx
|
|
115
|
+
@pixels[[x, y]] = color
|
|
116
|
+
@width = x + 1 if x + 1 > @width
|
|
117
|
+
end
|
|
118
|
+
@height = y + 1 if y + 1 > @height
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@cursor_x += count
|
|
122
|
+
@width = @cursor_x if @cursor_x > @width
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_repeat(data, i)
|
|
126
|
+
len = data.bytesize
|
|
127
|
+
num_str = +""
|
|
128
|
+
while i < len && data.getbyte(i) >= 0x30 && data.getbyte(i) <= 0x39
|
|
129
|
+
num_str << data.getbyte(i).chr
|
|
130
|
+
i += 1
|
|
131
|
+
end
|
|
132
|
+
count = num_str.empty? ? 1 : num_str.to_i
|
|
133
|
+
ch = nil
|
|
134
|
+
if i < len
|
|
135
|
+
byte = data.getbyte(i)
|
|
136
|
+
ch = byte if byte >= 0x3F && byte <= 0x7E
|
|
137
|
+
i += 1
|
|
138
|
+
end
|
|
139
|
+
[i, count, ch]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def parse_raster_attributes(data, i)
|
|
143
|
+
values, i = parse_numeric_params(data, i)
|
|
144
|
+
[i, values[0] || 0, values[1] || 0, values[2] || 0, values[3] || 0]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def parse_color(data, i)
|
|
148
|
+
values, i = parse_numeric_params(data, i)
|
|
149
|
+
|
|
150
|
+
if values.size == 1
|
|
151
|
+
@current_color = values[0]
|
|
152
|
+
elsif values.size >= 5
|
|
153
|
+
pc, pu, px, py, pz = values
|
|
154
|
+
if pu == 1
|
|
155
|
+
@color_registers[pc] = hls_to_rgb(px, py, pz)
|
|
156
|
+
else
|
|
157
|
+
@color_registers[pc] = [
|
|
158
|
+
(px * 255 / 100.0).round.clamp(0, 255),
|
|
159
|
+
(py * 255 / 100.0).round.clamp(0, 255),
|
|
160
|
+
(pz * 255 / 100.0).round.clamp(0, 255),
|
|
161
|
+
]
|
|
162
|
+
end
|
|
163
|
+
@current_color = pc
|
|
164
|
+
end
|
|
165
|
+
i
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def parse_numeric_params(data, i)
|
|
169
|
+
values = []
|
|
170
|
+
num_str = +""
|
|
171
|
+
len = data.bytesize
|
|
172
|
+
while i < len
|
|
173
|
+
byte = data.getbyte(i)
|
|
174
|
+
if byte >= 0x30 && byte <= 0x39
|
|
175
|
+
num_str << byte.chr
|
|
176
|
+
i += 1
|
|
177
|
+
elsif byte == 0x3B
|
|
178
|
+
values << (num_str.empty? ? 0 : num_str.to_i)
|
|
179
|
+
num_str = +""
|
|
180
|
+
i += 1
|
|
181
|
+
else
|
|
182
|
+
values << (num_str.empty? ? 0 : num_str.to_i)
|
|
183
|
+
break
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
[values, i]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def hls_to_rgb(h, l, s)
|
|
190
|
+
hh = h / 360.0
|
|
191
|
+
ll = l / 100.0
|
|
192
|
+
ss = s / 100.0
|
|
193
|
+
|
|
194
|
+
if ss == 0
|
|
195
|
+
v = (ll * 255).round.clamp(0, 255)
|
|
196
|
+
return [v, v, v]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
q = ll < 0.5 ? ll * (1.0 + ss) : ll + ss - ll * ss
|
|
200
|
+
p = 2.0 * ll - q
|
|
201
|
+
|
|
202
|
+
r = hue_to_rgb(p, q, hh + 1.0 / 3)
|
|
203
|
+
g = hue_to_rgb(p, q, hh)
|
|
204
|
+
b = hue_to_rgb(p, q, hh - 1.0 / 3)
|
|
205
|
+
|
|
206
|
+
[(r * 255).round.clamp(0, 255),
|
|
207
|
+
(g * 255).round.clamp(0, 255),
|
|
208
|
+
(b * 255).round.clamp(0, 255)]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def hue_to_rgb(p, q, t)
|
|
212
|
+
t += 1.0 if t < 0
|
|
213
|
+
t -= 1.0 if t > 1
|
|
214
|
+
return p + (q - p) * 6.0 * t if t < 1.0 / 6
|
|
215
|
+
return q if t < 1.0 / 2
|
|
216
|
+
return p + (q - p) * (2.0 / 3 - t) * 6.0 if t < 2.0 / 3
|
|
217
|
+
|
|
218
|
+
p
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
data/lib/echoes/tab.rb
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Echoes
|
|
4
|
+
class Tab
|
|
5
|
+
attr_reader :pane_tree
|
|
6
|
+
attr_accessor :title
|
|
7
|
+
|
|
8
|
+
def initialize(command:, rows:, cols:, cwd: nil, embedded: false, editor_file: nil)
|
|
9
|
+
@command = command
|
|
10
|
+
@rows = rows
|
|
11
|
+
@cols = cols
|
|
12
|
+
@embedded = embedded
|
|
13
|
+
pane = Pane.new(command: command, rows: rows, cols: cols, cwd: cwd,
|
|
14
|
+
embedded: embedded, editor_file: editor_file)
|
|
15
|
+
@pane_tree = PaneTree.new(pane)
|
|
16
|
+
@title = pane.title
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# --- Active pane delegation (backward compatibility) ---
|
|
20
|
+
|
|
21
|
+
def active_pane
|
|
22
|
+
@pane_tree.active_pane
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def screen
|
|
26
|
+
active_pane.screen
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parser
|
|
30
|
+
active_pane.parser
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pty_read
|
|
34
|
+
active_pane.pty_read
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pty_write
|
|
38
|
+
active_pane.pty_write
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pty_pid
|
|
42
|
+
active_pane.pty_pid
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write_input(bytes)
|
|
46
|
+
active_pane.write_input(bytes)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def submit_line(line)
|
|
50
|
+
active_pane.submit_line(line)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def read_available_output(max = 16384)
|
|
54
|
+
active_pane.read_available_output(max)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def scroll_offset
|
|
58
|
+
active_pane.scroll_offset
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def scroll_offset=(val)
|
|
62
|
+
active_pane.scroll_offset = val
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def scroll_accum
|
|
66
|
+
active_pane.scroll_accum
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def scroll_accum=(val)
|
|
70
|
+
active_pane.scroll_accum = val
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# --- Pane operations ---
|
|
74
|
+
|
|
75
|
+
def split_vertical
|
|
76
|
+
new_pane = create_pane
|
|
77
|
+
layout = @pane_tree.layout(0, 0, @cols, @rows)
|
|
78
|
+
current_rect = layout.find { |r| r[:pane] == active_pane }
|
|
79
|
+
if current_rect
|
|
80
|
+
half_cols = current_rect[:w] / 2
|
|
81
|
+
active_pane.resize(current_rect[:h], half_cols)
|
|
82
|
+
new_pane.resize(current_rect[:h], current_rect[:w] - half_cols)
|
|
83
|
+
end
|
|
84
|
+
@pane_tree.split(active_pane, :vertical, new_pane)
|
|
85
|
+
new_pane
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def split_horizontal
|
|
89
|
+
new_pane = create_pane
|
|
90
|
+
layout = @pane_tree.layout(0, 0, @cols, @rows)
|
|
91
|
+
current_rect = layout.find { |r| r[:pane] == active_pane }
|
|
92
|
+
if current_rect
|
|
93
|
+
half_rows = current_rect[:h] / 2
|
|
94
|
+
active_pane.resize(half_rows, current_rect[:w])
|
|
95
|
+
new_pane.resize(current_rect[:h] - half_rows, current_rect[:w])
|
|
96
|
+
end
|
|
97
|
+
@pane_tree.split(active_pane, :horizontal, new_pane)
|
|
98
|
+
new_pane
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def close_active_pane
|
|
102
|
+
return false if @pane_tree.single_pane?
|
|
103
|
+
|
|
104
|
+
pane = active_pane
|
|
105
|
+
@pane_tree.remove(pane)
|
|
106
|
+
pane.close
|
|
107
|
+
resize_panes
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def next_pane
|
|
112
|
+
@pane_tree.active_pane = @pane_tree.next_pane(active_pane)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def prev_pane
|
|
116
|
+
@pane_tree.active_pane = @pane_tree.prev_pane(active_pane)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def panes
|
|
120
|
+
@pane_tree.panes
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# --- Lifecycle ---
|
|
124
|
+
|
|
125
|
+
def alive?
|
|
126
|
+
panes.any?(&:alive?)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def resize(rows, cols)
|
|
130
|
+
@rows = rows
|
|
131
|
+
@cols = cols
|
|
132
|
+
resize_panes
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def close
|
|
136
|
+
panes.each(&:close)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def create_pane
|
|
142
|
+
Pane.new(command: @command, rows: @rows, cols: @cols)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def resize_panes
|
|
146
|
+
layout = @pane_tree.layout(0, 0, @cols, @rows)
|
|
147
|
+
layout.each do |rect|
|
|
148
|
+
rect[:pane].resize(rect[:h], rect[:w])
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pty'
|
|
4
|
+
require 'io/console'
|
|
5
|
+
|
|
6
|
+
module Echoes
|
|
7
|
+
class Terminal
|
|
8
|
+
attr_reader :screen
|
|
9
|
+
|
|
10
|
+
def initialize(command: Echoes.config.shell, rows: nil, cols: nil)
|
|
11
|
+
size = IO.console&.winsize || [24, 80]
|
|
12
|
+
@rows = rows || size[0]
|
|
13
|
+
@cols = cols || size[1]
|
|
14
|
+
@command = command
|
|
15
|
+
@screen = Screen.new(rows: @rows, cols: @cols)
|
|
16
|
+
@parser = Parser.new(@screen, writer: ->(s) { @write_io&.write(s) rescue nil })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
PTY.spawn(@command) do |read_io, write_io, pid|
|
|
21
|
+
@read_io = read_io
|
|
22
|
+
@write_io = write_io
|
|
23
|
+
@pid = pid
|
|
24
|
+
|
|
25
|
+
@read_io.winsize = [@rows, @cols]
|
|
26
|
+
|
|
27
|
+
setup_signal_handlers
|
|
28
|
+
|
|
29
|
+
STDIN.raw do
|
|
30
|
+
reader = Thread.new { read_loop }
|
|
31
|
+
write_loop
|
|
32
|
+
reader.kill
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def read_loop
|
|
40
|
+
loop do
|
|
41
|
+
data = @read_io.read_nonblock(4096)
|
|
42
|
+
@parser.feed(data)
|
|
43
|
+
render
|
|
44
|
+
rescue IO::WaitReadable
|
|
45
|
+
IO.select([@read_io])
|
|
46
|
+
retry
|
|
47
|
+
rescue EOFError, Errno::EIO
|
|
48
|
+
break
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write_loop
|
|
53
|
+
loop do
|
|
54
|
+
data = STDIN.read_nonblock(4096)
|
|
55
|
+
@write_io.write(data)
|
|
56
|
+
rescue IO::WaitReadable
|
|
57
|
+
IO.select([STDIN])
|
|
58
|
+
retry
|
|
59
|
+
rescue EOFError, Errno::EIO
|
|
60
|
+
break
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render
|
|
65
|
+
buf = +"\e[H"
|
|
66
|
+
last_fg = nil
|
|
67
|
+
last_bg = nil
|
|
68
|
+
last_bold = false
|
|
69
|
+
last_underline = false
|
|
70
|
+
last_inverse = false
|
|
71
|
+
|
|
72
|
+
@screen.grid.each_with_index do |row, r|
|
|
73
|
+
row.each do |cell|
|
|
74
|
+
if cell.fg != last_fg || cell.bg != last_bg || cell.bold != last_bold ||
|
|
75
|
+
cell.underline != last_underline || cell.inverse != last_inverse
|
|
76
|
+
codes = [0]
|
|
77
|
+
codes << 1 if cell.bold
|
|
78
|
+
codes << 2 if cell.faint
|
|
79
|
+
codes << 3 if cell.italic
|
|
80
|
+
codes << 4 if cell.underline
|
|
81
|
+
codes << 5 if cell.blink
|
|
82
|
+
codes << 7 if cell.inverse
|
|
83
|
+
codes << 8 if cell.concealed
|
|
84
|
+
codes << 9 if cell.strikethrough
|
|
85
|
+
if cell.fg.is_a?(Array)
|
|
86
|
+
codes.push(38, 2, *cell.fg)
|
|
87
|
+
elsif cell.fg
|
|
88
|
+
codes << (cell.fg < 8 ? cell.fg + 30 : cell.fg - 8 + 90)
|
|
89
|
+
end
|
|
90
|
+
if cell.bg.is_a?(Array)
|
|
91
|
+
codes.push(48, 2, *cell.bg)
|
|
92
|
+
elsif cell.bg
|
|
93
|
+
codes << (cell.bg < 8 ? cell.bg + 40 : cell.bg - 8 + 100)
|
|
94
|
+
end
|
|
95
|
+
buf << "\e[#{codes.join(';')}m"
|
|
96
|
+
last_fg = cell.fg
|
|
97
|
+
last_bg = cell.bg
|
|
98
|
+
last_bold = cell.bold
|
|
99
|
+
last_underline = cell.underline
|
|
100
|
+
last_inverse = cell.inverse
|
|
101
|
+
end
|
|
102
|
+
buf << cell.char
|
|
103
|
+
end
|
|
104
|
+
buf << "\r\n" unless r == @screen.rows - 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
buf << "\e[0m"
|
|
108
|
+
buf << "\e[#{@screen.cursor.row + 1};#{@screen.cursor.col + 1}H"
|
|
109
|
+
buf << (@screen.cursor.visible ? "\e[?25h" : "\e[?25l")
|
|
110
|
+
STDOUT.write(buf)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def setup_signal_handlers
|
|
114
|
+
Signal.trap(:WINCH) do
|
|
115
|
+
if IO.console
|
|
116
|
+
@rows, @cols = IO.console.winsize
|
|
117
|
+
@screen.resize(@rows, @cols)
|
|
118
|
+
@read_io.winsize = [@rows, @cols]
|
|
119
|
+
render
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/echoes.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# When invoked outside Bundler (e.g. from the .app launcher with bare
|
|
4
|
+
# `ruby exe/echoes`), `require 'rubish'` and `require 'rvim'` would
|
|
5
|
+
# pick up gem-installed copies that may lag the path-pinned source
|
|
6
|
+
# in the Gemfile and lack methods echoes calls. Prepend the sibling
|
|
7
|
+
# repo lib paths so the source versions win over Rubygems' default
|
|
8
|
+
# activation. The Gemfile already encodes the same sibling assumption
|
|
9
|
+
# via `path: "../rubish"` / `path: "../rvim"`, so this is just the
|
|
10
|
+
# Bundler-less mirror of that.
|
|
11
|
+
%w[rubish rvim].each do |sibling|
|
|
12
|
+
lib = File.expand_path("../../#{sibling}/lib", __dir__)
|
|
13
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require_relative "echoes/version"
|
|
17
|
+
require_relative "echoes/configuration"
|
|
18
|
+
require_relative "echoes/cell"
|
|
19
|
+
require_relative "echoes/cursor"
|
|
20
|
+
require_relative "echoes/screen"
|
|
21
|
+
require_relative "echoes/parser"
|
|
22
|
+
require_relative "echoes/copy_mode"
|
|
23
|
+
require_relative "echoes/pane"
|
|
24
|
+
require_relative "echoes/pane_tree"
|
|
25
|
+
require_relative "echoes/tab"
|
|
26
|
+
require_relative "echoes/sixel_decoder"
|
|
27
|
+
require_relative "echoes/terminal"
|
|
28
|
+
require_relative "echoes/objc"
|
|
29
|
+
require_relative "echoes/preferences"
|
|
30
|
+
require_relative "echoes/client"
|
|
31
|
+
require_relative "echoes/gui"
|
|
32
|
+
|
|
33
|
+
module Echoes
|
|
34
|
+
class Error < StandardError; end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Echoes.load_config
|
data/sig/echoes.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: echoes
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Akira Matsuda
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: syslog
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
-
|
|
17
|
+
- ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: "0"
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
-
|
|
25
|
+
- ">="
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: "0"
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: fiddle
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
-
|
|
33
|
+
- ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: "0"
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
-
|
|
41
|
+
- ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: "0"
|
|
44
|
+
description: |
|
|
45
|
+
Echoes is a pure-Ruby macOS terminal emulator with first-class
|
|
46
|
+
integrations for rubish (in-process shell) and rvim (in-process
|
|
47
|
+
vim editor) panes, plus a private OSC namespace for in-pane
|
|
48
|
+
Ruby tools that want to drive UI features (gradient backgrounds,
|
|
49
|
+
rectangular fills, proportional-font text) other terminals
|
|
50
|
+
can't. Written in pure Ruby on top of AppKit via Fiddle.
|
|
51
|
+
email:
|
|
52
|
+
- ronnie@dio.jp
|
|
53
|
+
executables:
|
|
54
|
+
- echoes
|
|
55
|
+
extensions: []
|
|
56
|
+
extra_rdoc_files: []
|
|
57
|
+
files:
|
|
58
|
+
- CLAUDE.md
|
|
59
|
+
- Echoes.app/Contents/Info.plist
|
|
60
|
+
- Echoes.app/Contents/MacOS/Echoes
|
|
61
|
+
- EchoesEmbed.app/Contents/Info.plist
|
|
62
|
+
- EchoesEmbed.app/Contents/MacOS/EchoesEmbed
|
|
63
|
+
- LICENSE.txt
|
|
64
|
+
- README.md
|
|
65
|
+
- Rakefile
|
|
66
|
+
- exe/echoes
|
|
67
|
+
- lib/echoes.rb
|
|
68
|
+
- lib/echoes/cell.rb
|
|
69
|
+
- lib/echoes/client.rb
|
|
70
|
+
- lib/echoes/configuration.rb
|
|
71
|
+
- lib/echoes/copy_mode.rb
|
|
72
|
+
- lib/echoes/cursor.rb
|
|
73
|
+
- lib/echoes/editor.rb
|
|
74
|
+
- lib/echoes/embedded_shell.rb
|
|
75
|
+
- lib/echoes/embedded_shell_helper.rb
|
|
76
|
+
- lib/echoes/gui.rb
|
|
77
|
+
- lib/echoes/installer.rb
|
|
78
|
+
- lib/echoes/objc.rb
|
|
79
|
+
- lib/echoes/pane.rb
|
|
80
|
+
- lib/echoes/pane_tree.rb
|
|
81
|
+
- lib/echoes/parser.rb
|
|
82
|
+
- lib/echoes/preferences.rb
|
|
83
|
+
- lib/echoes/screen.rb
|
|
84
|
+
- lib/echoes/sixel_decoder.rb
|
|
85
|
+
- lib/echoes/tab.rb
|
|
86
|
+
- lib/echoes/terminal.rb
|
|
87
|
+
- lib/echoes/version.rb
|
|
88
|
+
- sig/echoes.rbs
|
|
89
|
+
homepage: "https://github.com/amatsuda/echoes"
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
homepage_uri: "https://github.com/amatsuda/echoes"
|
|
94
|
+
source_code_uri: "https://github.com/amatsuda/echoes"
|
|
95
|
+
post_install_message: |
|
|
96
|
+
To launch Echoes from Spotlight / Dock / Cmd-Space, run:
|
|
97
|
+
|
|
98
|
+
echoes install
|
|
99
|
+
|
|
100
|
+
This drops thin Echoes.app and EchoesEmbed.app shortcuts in
|
|
101
|
+
~/Applications/ that exec into the real gem-bundled launchers.
|
|
102
|
+
Re-run `echoes install` after each `gem update echoes` to refresh
|
|
103
|
+
the shortcuts; `echoes uninstall` removes them.
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
-
|
|
110
|
+
- ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: 3.2.0
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
-
|
|
116
|
+
- ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: "0"
|
|
119
|
+
requirements: []
|
|
120
|
+
rubygems_version: 4.1.0.dev
|
|
121
|
+
specification_version: 4
|
|
122
|
+
summary: A pure-Ruby AppKit-based macOS terminal emulator.
|
|
123
|
+
test_files: []
|