echoes 0.2.0 → 0.3.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/lib/echoes/client.rb +30 -9
- data/lib/echoes/configuration.rb +131 -0
- data/lib/echoes/embedded_shell.rb +9 -4
- data/lib/echoes/gui.rb +962 -108
- data/lib/echoes/iterm2_images.rb +122 -0
- data/lib/echoes/kitty_graphics.rb +320 -0
- data/lib/echoes/kitty_graphics_appkit.rb +174 -0
- data/lib/echoes/objc.rb +2 -0
- data/lib/echoes/pane.rb +108 -9
- data/lib/echoes/parser.rb +145 -10
- data/lib/echoes/profile.rb +75 -0
- data/lib/echoes/screen.rb +144 -9
- data/lib/echoes/tab.rb +2 -2
- data/lib/echoes/version.rb +1 -1
- metadata +5 -1
data/lib/echoes/pane.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Echoes
|
|
|
17
17
|
:scroll_offset, :scroll_accum, :title, :copy_mode
|
|
18
18
|
attr_reader :embedded_shell
|
|
19
19
|
|
|
20
|
-
def initialize(command:, rows:, cols:, cwd: nil, embedded: false, no_rc: false, editor_file: nil)
|
|
20
|
+
def initialize(command:, rows:, cols:, cwd: nil, embedded: false, no_rc: false, editor_file: nil, env: nil)
|
|
21
21
|
@screen = Screen.new(rows: rows, cols: cols)
|
|
22
22
|
if editor_file
|
|
23
23
|
require_relative 'editor'
|
|
@@ -27,7 +27,13 @@ module Echoes
|
|
|
27
27
|
elsif embedded
|
|
28
28
|
require_relative 'embedded_shell'
|
|
29
29
|
@embedded_shell = EmbeddedShell.new(no_rc: no_rc)
|
|
30
|
-
|
|
30
|
+
# Writer routes OSC replies (display-info, OSC 52 paste-back,
|
|
31
|
+
# color queries, terminfo replies, …) to the helper's pty
|
|
32
|
+
# master so the foreground program reads them on its stdin.
|
|
33
|
+
# While rubish is at the prompt, anything we write here lands
|
|
34
|
+
# in Reline; in practice query OSCs only come from a running
|
|
35
|
+
# foreground program (e.g. przn) so the routing is safe.
|
|
36
|
+
@parser = Parser.new(@screen, writer: ->(s) { @embedded_shell.forward_input(s) })
|
|
31
37
|
@title = 'rubish'
|
|
32
38
|
@input_buffer = +''
|
|
33
39
|
@input_cursor = 0 # offset within @input_buffer (0..length)
|
|
@@ -47,14 +53,27 @@ module Echoes
|
|
|
47
53
|
else
|
|
48
54
|
start_dir = (cwd && Dir.exist?(cwd)) ? cwd : Dir.home
|
|
49
55
|
Dir.chdir(start_dir) do
|
|
56
|
+
# When env: is nil (the default), we just normalize a few
|
|
57
|
+
# vars on our own process — the child then inherits the
|
|
58
|
+
# whole env from us, same as before. When env: is given,
|
|
59
|
+
# it's an explicit env Hash that fully replaces what the
|
|
60
|
+
# child sees, with our normalizations applied on top. The
|
|
61
|
+
# explicit form is used by the OSC 7772 ;open-window path
|
|
62
|
+
# so a child program (e.g. przn) reliably gets PATH /
|
|
63
|
+
# HOME / USER / LANG even when Echoes.app was launched by
|
|
64
|
+
# launchd with a minimal env.
|
|
50
65
|
ENV['TERM'] = Echoes.config.term
|
|
51
66
|
ENV['LANG'] ||= 'en_US.UTF-8'
|
|
52
67
|
ENV['LC_CTYPE'] = 'UTF-8'
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
# `command` may be a String (shell-parsed by /bin/sh) or an
|
|
69
|
+
# Array of [argv0, *args] (execve directly, no shell). The
|
|
70
|
+
# array form is what the OSC 7772 ;open-window handler
|
|
71
|
+
# uses so user-supplied argv isn't subject to shell quoting.
|
|
72
|
+
spawn_args = command.is_a?(Array) ? command : [command]
|
|
73
|
+
@pty_read, @pty_write, @pty_pid = spawn_with_pty(spawn_args, env, rows, cols)
|
|
55
74
|
end
|
|
56
75
|
@parser = Parser.new(@screen, writer: ->(s) { @pty_write.write(s) rescue nil })
|
|
57
|
-
@title = File.basename(command)
|
|
76
|
+
@title = File.basename(command.is_a?(Array) ? command.first : command)
|
|
58
77
|
end
|
|
59
78
|
@scroll_offset = 0
|
|
60
79
|
@scroll_accum = 0.0
|
|
@@ -89,7 +108,10 @@ module Echoes
|
|
|
89
108
|
# embedded mode hands the line directly to the in-process REPL.
|
|
90
109
|
def submit_line(line)
|
|
91
110
|
if embedded?
|
|
92
|
-
@embedded_shell.submit_line(line
|
|
111
|
+
@embedded_shell.submit_line(line,
|
|
112
|
+
rows: @screen.rows, cols: @screen.cols,
|
|
113
|
+
px_width: pty_pixel_width(@screen.cols),
|
|
114
|
+
px_height: pty_pixel_height(@screen.rows))
|
|
93
115
|
else
|
|
94
116
|
@pty_write.write("#{line}\r") rescue nil
|
|
95
117
|
end
|
|
@@ -145,9 +167,30 @@ module Echoes
|
|
|
145
167
|
@editor.resize(rows: rows, cols: cols)
|
|
146
168
|
render_editor
|
|
147
169
|
elsif embedded?
|
|
148
|
-
@embedded_shell.resize(rows: rows, cols: cols
|
|
170
|
+
@embedded_shell.resize(rows: rows, cols: cols,
|
|
171
|
+
px_width: pty_pixel_width(cols),
|
|
172
|
+
px_height: pty_pixel_height(rows))
|
|
149
173
|
else
|
|
150
|
-
@pty_read.winsize =
|
|
174
|
+
@pty_read.winsize = pty_winsize_quad(rows, cols)
|
|
175
|
+
end
|
|
176
|
+
rescue Errno::EIO, IOError
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Re-send winsize with the current cell pixel metrics. Called
|
|
180
|
+
# by the GUI after `wire_screen_handlers` updates the Screen's
|
|
181
|
+
# cell_pixel_width / cell_pixel_height (font load, font size
|
|
182
|
+
# change, …), so TIOCGWINSZ on the slave side carries the
|
|
183
|
+
# right pixel dims — kitten icat and other image protocols
|
|
184
|
+
# read those instead of querying CSI 14 t.
|
|
185
|
+
def refresh_pty_pixel_size
|
|
186
|
+
rows = @screen.rows
|
|
187
|
+
cols = @screen.cols
|
|
188
|
+
if embedded?
|
|
189
|
+
@embedded_shell.resize(rows: rows, cols: cols,
|
|
190
|
+
px_width: pty_pixel_width(cols),
|
|
191
|
+
px_height: pty_pixel_height(rows))
|
|
192
|
+
elsif @pty_read && !@pty_read.closed?
|
|
193
|
+
@pty_read.winsize = pty_winsize_quad(rows, cols)
|
|
151
194
|
end
|
|
152
195
|
rescue Errno::EIO, IOError
|
|
153
196
|
end
|
|
@@ -484,7 +527,9 @@ module Echoes
|
|
|
484
527
|
# Stash the command text on the OSC 133 mark so click-to-rerun
|
|
485
528
|
# can recover it later.
|
|
486
529
|
@screen.set_current_command_text(line)
|
|
487
|
-
@embedded_shell.submit_line(line, rows: @screen.rows, cols: @screen.cols
|
|
530
|
+
@embedded_shell.submit_line(line, rows: @screen.rows, cols: @screen.cols,
|
|
531
|
+
px_width: pty_pixel_width(@screen.cols),
|
|
532
|
+
px_height: pty_pixel_height(@screen.rows))
|
|
488
533
|
@embedded_running = true
|
|
489
534
|
end
|
|
490
535
|
end
|
|
@@ -828,6 +873,60 @@ module Echoes
|
|
|
828
873
|
|
|
829
874
|
private
|
|
830
875
|
|
|
876
|
+
# macOS ioctl numbers used by the manual pty setup below.
|
|
877
|
+
# PTY.spawn does setsid + TIOCSCTTY, but skips tcsetpgrp —
|
|
878
|
+
# leaving the slave's foreground process group unset (the
|
|
879
|
+
# macOS kernel doesn't fill it in automatically the way Linux
|
|
880
|
+
# does on TIOCSCTTY). The user's shell rc files then run
|
|
881
|
+
# things like `stty -ixon`, zsh fork+setpgid's stty into its
|
|
882
|
+
# own group for job control, that group has no parent in
|
|
883
|
+
# the slave's session, and tcsetattr returns
|
|
884
|
+
# stty: tcsetattr: Input/output error
|
|
885
|
+
# because the calling group is "orphaned and not foreground".
|
|
886
|
+
# Pre-seeding the foreground pgrp to the shell's pid in the
|
|
887
|
+
# child — same way embedded_shell_helper does — makes the
|
|
888
|
+
# whole startup path tcsetattr-safe.
|
|
889
|
+
DARWIN_TIOCSCTTY = 0x20007461
|
|
890
|
+
DARWIN_TIOCSPGRP = 0x80047476
|
|
891
|
+
|
|
892
|
+
def spawn_with_pty(spawn_args, env, rows, cols)
|
|
893
|
+
master, slave = PTY.open
|
|
894
|
+
slave.winsize = pty_winsize_quad(rows, cols)
|
|
895
|
+
pid = fork do
|
|
896
|
+
master.close
|
|
897
|
+
Process.setsid rescue nil
|
|
898
|
+
slave.ioctl(DARWIN_TIOCSCTTY, 0) rescue nil
|
|
899
|
+
slave.ioctl(DARWIN_TIOCSPGRP, [Process.getpgrp].pack('i!')) rescue nil
|
|
900
|
+
STDIN.reopen(slave)
|
|
901
|
+
STDOUT.reopen(slave)
|
|
902
|
+
STDERR.reopen(slave)
|
|
903
|
+
slave.close rescue nil
|
|
904
|
+
if env
|
|
905
|
+
exec(env, *spawn_args)
|
|
906
|
+
else
|
|
907
|
+
exec(*spawn_args)
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
slave.close
|
|
911
|
+
[master, master, pid]
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
# 4-element winsize tuple [rows, cols, xpixel, ypixel] for
|
|
915
|
+
# TIOCSWINSZ. The pixel fields seed the slave's TIOCGWINSZ so
|
|
916
|
+
# processes that prefer ioctl over CSI 14 t (kitten icat,
|
|
917
|
+
# tput, …) see real screen pixel dims.
|
|
918
|
+
def pty_winsize_quad(rows, cols)
|
|
919
|
+
[rows, cols, pty_pixel_width(cols), pty_pixel_height(rows)]
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def pty_pixel_width(cols)
|
|
923
|
+
(cols * @screen.cell_pixel_width).to_i
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def pty_pixel_height(rows)
|
|
927
|
+
(rows * @screen.cell_pixel_height).to_i
|
|
928
|
+
end
|
|
929
|
+
|
|
831
930
|
def complete_input
|
|
832
931
|
req = completion_request
|
|
833
932
|
return unless req
|
data/lib/echoes/parser.rb
CHANGED
|
@@ -30,8 +30,12 @@ module Echoes
|
|
|
30
30
|
|
|
31
31
|
REPLACEMENT_CHAR = "\u{FFFD}"
|
|
32
32
|
CSI_PARAM_LIMIT = 32
|
|
33
|
-
|
|
33
|
+
# 16 MB to accommodate OSC 1337 ;File= base64-encoded image
|
|
34
|
+
# payloads (iTerm2 inline images). Non-image OSCs are short
|
|
35
|
+
# and don't notice the higher cap.
|
|
36
|
+
OSC_BUFFER_LIMIT = 16 * 1024 * 1024
|
|
34
37
|
DCS_BUFFER_LIMIT = 1024 * 1024 # 1 MB (sixel images can be large)
|
|
38
|
+
APC_BUFFER_LIMIT = 16 * 1024 * 1024 # 16 MB (kitty graphics can be big)
|
|
35
39
|
|
|
36
40
|
def process_byte(byte)
|
|
37
41
|
# UTF-8 continuation bytes
|
|
@@ -72,6 +76,8 @@ module Echoes
|
|
|
72
76
|
csi_param(byte)
|
|
73
77
|
when :osc_string
|
|
74
78
|
osc_string(byte)
|
|
79
|
+
when :apc_string
|
|
80
|
+
apc_string(byte)
|
|
75
81
|
when :dcs_entry
|
|
76
82
|
dcs_entry(byte)
|
|
77
83
|
when :dcs_param
|
|
@@ -137,6 +143,9 @@ module Echoes
|
|
|
137
143
|
when 0x5D # ]
|
|
138
144
|
@state = :osc_string
|
|
139
145
|
@osc_string = "".b
|
|
146
|
+
when 0x5F # _ (APC — Application Program Command)
|
|
147
|
+
@state = :apc_string
|
|
148
|
+
@apc_string = "".b
|
|
140
149
|
when 0x37 # 7
|
|
141
150
|
@screen.save_cursor
|
|
142
151
|
@state = :ground
|
|
@@ -289,6 +298,27 @@ module Echoes
|
|
|
289
298
|
end
|
|
290
299
|
end
|
|
291
300
|
|
|
301
|
+
# APC (Application Program Command) accumulator. Mirrors the
|
|
302
|
+
# OSC state shape: bytes accumulate until BEL or ST (`ESC \`),
|
|
303
|
+
# then `dispatch_apc` routes by namespace prefix. Currently
|
|
304
|
+
# only the kitty graphics protocol (`G…`) lives in here.
|
|
305
|
+
def apc_string(byte)
|
|
306
|
+
case byte
|
|
307
|
+
when 0x07 # BEL terminates APC
|
|
308
|
+
dispatch_apc
|
|
309
|
+
@state = :ground
|
|
310
|
+
when 0x1B # ESC — dispatch, enter escape state for ST (\)
|
|
311
|
+
dispatch_apc
|
|
312
|
+
@state = :escape
|
|
313
|
+
else
|
|
314
|
+
if @apc_string.bytesize < APC_BUFFER_LIMIT
|
|
315
|
+
@apc_string << byte
|
|
316
|
+
else
|
|
317
|
+
@state = :ground
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
292
322
|
def dcs_entry(byte)
|
|
293
323
|
case byte
|
|
294
324
|
when 0x30..0x39
|
|
@@ -380,6 +410,23 @@ module Echoes
|
|
|
380
410
|
end
|
|
381
411
|
end
|
|
382
412
|
|
|
413
|
+
# APC body shape: G<comma-separated-options>;<base64-payload>
|
|
414
|
+
# Anything not starting with `G` is some other private APC
|
|
415
|
+
# subprotocol we don't handle; ignore silently.
|
|
416
|
+
def dispatch_apc
|
|
417
|
+
body = @apc_string
|
|
418
|
+
return if body.empty?
|
|
419
|
+
return unless body.getbyte(0) == 0x47 # 'G'
|
|
420
|
+
meta_and_payload = body.byteslice(1..) || ''.b
|
|
421
|
+
meta, payload = meta_and_payload.split(';'.b, 2)
|
|
422
|
+
meta = (meta || '').force_encoding('UTF-8')
|
|
423
|
+
payload = (payload || ''.b)
|
|
424
|
+
require_relative 'kitty_graphics'
|
|
425
|
+
@kitty_state ||= {chunks: {}, cache: {}}
|
|
426
|
+
KittyGraphics.handle_chunk(@kitty_state, meta, payload,
|
|
427
|
+
screen: @screen, writer: @writer)
|
|
428
|
+
end
|
|
429
|
+
|
|
383
430
|
def dispatch_osc
|
|
384
431
|
code, rest = @osc_string.split(';'.b, 2)
|
|
385
432
|
return unless rest
|
|
@@ -413,12 +460,25 @@ module Echoes
|
|
|
413
460
|
when '52'
|
|
414
461
|
dispatch_osc52(rest)
|
|
415
462
|
return
|
|
463
|
+
when '9'
|
|
464
|
+
# OSC 9 (iTerm2): everything after `9;` is the message body.
|
|
465
|
+
# Use the pane title as the notification title; the host
|
|
466
|
+
# decides how to render it.
|
|
467
|
+
deliver_notification(nil, rest)
|
|
468
|
+
return
|
|
469
|
+
when '777'
|
|
470
|
+
dispatch_osc777(rest)
|
|
471
|
+
return
|
|
416
472
|
when '133'
|
|
417
473
|
dispatch_osc133(rest)
|
|
418
474
|
return
|
|
419
475
|
when '7772'
|
|
420
476
|
dispatch_osc7772(rest)
|
|
421
477
|
return
|
|
478
|
+
when '1337'
|
|
479
|
+
require_relative 'iterm2_images'
|
|
480
|
+
Iterm2Images.handle(rest, screen: @screen, writer: @writer)
|
|
481
|
+
return
|
|
422
482
|
when '66'
|
|
423
483
|
# fall through to multicell handling below
|
|
424
484
|
else
|
|
@@ -428,12 +488,20 @@ module Echoes
|
|
|
428
488
|
return unless text
|
|
429
489
|
|
|
430
490
|
text.force_encoding('UTF-8')
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
491
|
+
@screen.put_multicell(text, **parse_multicell_meta(meta_str, allow_extensions: false))
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Parse the `key=value:key=value:...` meta block that lives in
|
|
495
|
+
# the second field of OSC 66 (and of OSC 7772 ;multicell).
|
|
496
|
+
# `allow_extensions: false` accepts only the kitty spec keys
|
|
497
|
+
# (s/w/n/d/v/h) so OSC 66 stays strictly compatible with other
|
|
498
|
+
# terminals — Echoes-private knobs (f=family, flip=h|v|hv) are
|
|
499
|
+
# routed through OSC 7772 ;multicell, where collisions with a
|
|
500
|
+
# future kitty spec extension can't surprise emitters.
|
|
501
|
+
def parse_multicell_meta(meta_str, allow_extensions:)
|
|
502
|
+
params = {scale: 1, width: 0, frac_n: 0, frac_d: 0, valign: 0, halign: 0,
|
|
503
|
+
family: nil, flip_h: false, flip_v: false}
|
|
504
|
+
meta_str.to_s.split(':').each do |pair|
|
|
437
505
|
k, v = pair.split('=', 2)
|
|
438
506
|
next unless v
|
|
439
507
|
case k
|
|
@@ -443,11 +511,24 @@ module Echoes
|
|
|
443
511
|
when 'd' then params[:frac_d] = v.to_i.clamp(0, 15)
|
|
444
512
|
when 'v' then params[:valign] = v.to_i.clamp(0, 2)
|
|
445
513
|
when 'h' then params[:halign] = v.to_i.clamp(0, 2)
|
|
446
|
-
when 'f'
|
|
514
|
+
when 'f'
|
|
515
|
+
# Family name. Names with ':' aren't representable here
|
|
516
|
+
# because ':' is the meta-field separator — use ',' or
|
|
517
|
+
# omit the colon (e.g. "Helvetica Neue", not "Foo:Italic").
|
|
518
|
+
params[:family] = v if allow_extensions && !v.empty?
|
|
519
|
+
when 'flip'
|
|
520
|
+
# Mirror the rendered glyph(s). `flip=h` horizontal,
|
|
521
|
+
# `flip=v` vertical, `flip=hv` / `flip=vh` both. Handy
|
|
522
|
+
# for direction-having emojis (e.g. flipping 🐇 to face
|
|
523
|
+
# the other way).
|
|
524
|
+
if allow_extensions
|
|
525
|
+
flip = v.downcase
|
|
526
|
+
params[:flip_h] = flip.include?('h')
|
|
527
|
+
params[:flip_v] = flip.include?('v')
|
|
528
|
+
end
|
|
447
529
|
end
|
|
448
530
|
end
|
|
449
|
-
|
|
450
|
-
@screen.put_multicell(text, **params)
|
|
531
|
+
params
|
|
451
532
|
end
|
|
452
533
|
|
|
453
534
|
def dispatch_osc_default_color(key, osc_code, spec)
|
|
@@ -521,9 +602,26 @@ module Echoes
|
|
|
521
602
|
# bg-gradient ; type=linear:angle=N:colors=#rrggbb,#rrggbb[,...]
|
|
522
603
|
# bg-fill ; color=#rrggbb:rect=row1,col1,row2,col2
|
|
523
604
|
# bg-clear (revert to default_bg)
|
|
605
|
+
# capture ; <absolute-path-to.png>
|
|
606
|
+
# display-info (sync query — host writes
|
|
607
|
+
# \e]7772;display-info;<json>\a
|
|
608
|
+
# back to the pty)
|
|
609
|
+
# open-window ; display=N:program=<base64-argv>:fullscreen=yes|no
|
|
610
|
+
# multicell ; <params> ; <text> (OSC-66-shaped multicell + the
|
|
611
|
+
# Echoes-only knobs `f=family`
|
|
612
|
+
# and `flip=h|v|hv`. Use this
|
|
613
|
+
# instead of putting extensions
|
|
614
|
+
# on OSC 66 so portable emitters
|
|
615
|
+
# can keep OSC 66 strictly
|
|
616
|
+
# kitty-compatible.)
|
|
524
617
|
def dispatch_osc7772(rest)
|
|
525
618
|
command, args = rest.split(';', 2)
|
|
526
619
|
case command
|
|
620
|
+
when 'multicell'
|
|
621
|
+
meta_str, text = (args || '').split(';', 2)
|
|
622
|
+
return unless text
|
|
623
|
+
text.force_encoding('UTF-8')
|
|
624
|
+
@screen.put_multicell(text, **parse_multicell_meta(meta_str, allow_extensions: true))
|
|
527
625
|
when 'bg-color'
|
|
528
626
|
rgba = parse_hex_color((args || '').strip)
|
|
529
627
|
if rgba
|
|
@@ -546,6 +644,20 @@ module Echoes
|
|
|
546
644
|
@screen.background = nil
|
|
547
645
|
@screen.bg_fills.clear if @screen.respond_to?(:bg_fills) && @screen.bg_fills
|
|
548
646
|
@screen.mark_all_dirty if @screen.respond_to?(:mark_all_dirty)
|
|
647
|
+
when 'capture'
|
|
648
|
+
path = (args || '').strip
|
|
649
|
+
if !path.empty? && @screen.respond_to?(:capture_handler) && @screen.capture_handler
|
|
650
|
+
@screen.capture_handler.call(path)
|
|
651
|
+
end
|
|
652
|
+
when 'display-info'
|
|
653
|
+
if @screen.respond_to?(:display_info_handler) && @screen.display_info_handler
|
|
654
|
+
json = @screen.display_info_handler.call.to_s
|
|
655
|
+
@writer.call("\e]7772;display-info;#{json}\a") if @writer
|
|
656
|
+
end
|
|
657
|
+
when 'open-window'
|
|
658
|
+
if @screen.respond_to?(:open_window_handler) && @screen.open_window_handler
|
|
659
|
+
@screen.open_window_handler.call(args || '')
|
|
660
|
+
end
|
|
549
661
|
end
|
|
550
662
|
end
|
|
551
663
|
|
|
@@ -611,6 +723,27 @@ module Echoes
|
|
|
611
723
|
end
|
|
612
724
|
end
|
|
613
725
|
|
|
726
|
+
# OSC 777 — VTE-style notifications:
|
|
727
|
+
# \e]777;notify;<title>;<message>\a
|
|
728
|
+
# Some emitters omit the body, sending only a title.
|
|
729
|
+
def dispatch_osc777(rest)
|
|
730
|
+
parts = rest.split(';', 3)
|
|
731
|
+
return unless parts[0] == 'notify'
|
|
732
|
+
title = parts[1] || ''
|
|
733
|
+
message = parts[2] || ''
|
|
734
|
+
deliver_notification(title.empty? ? nil : title, message)
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Hand off to whatever the host wired into `screen.notification_handler`.
|
|
738
|
+
# Title is nil when the emitter (OSC 9) didn't supply one — the
|
|
739
|
+
# host typically falls back to the pane title or "Echoes". A
|
|
740
|
+
# missing handler is a no-op so the parser never raises on
|
|
741
|
+
# non-GUI tests.
|
|
742
|
+
def deliver_notification(title, message)
|
|
743
|
+
return unless @screen.respond_to?(:notification_handler) && @screen.notification_handler
|
|
744
|
+
@screen.notification_handler.call(title, message)
|
|
745
|
+
end
|
|
746
|
+
|
|
614
747
|
def dispatch_osc133(rest)
|
|
615
748
|
return unless @screen.respond_to?(:osc133_mark)
|
|
616
749
|
parts = rest.split(';')
|
|
@@ -712,6 +845,7 @@ module Echoes
|
|
|
712
845
|
when 1006 then @screen.mouse_encoding = :sgr
|
|
713
846
|
when 1004 then @screen.focus_reporting = true
|
|
714
847
|
when 2004 then @screen.bracketed_paste_mode = true
|
|
848
|
+
when 2026 then @screen.sync_active = true
|
|
715
849
|
when 1049
|
|
716
850
|
@screen.save_cursor
|
|
717
851
|
@screen.switch_to_alt_screen
|
|
@@ -741,6 +875,7 @@ module Echoes
|
|
|
741
875
|
when 1006 then @screen.mouse_encoding = :default
|
|
742
876
|
when 1004 then @screen.focus_reporting = false
|
|
743
877
|
when 2004 then @screen.bracketed_paste_mode = false
|
|
878
|
+
when 2026 then @screen.sync_active = false
|
|
744
879
|
when 1049
|
|
745
880
|
@screen.switch_to_main_screen
|
|
746
881
|
@screen.restore_cursor
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Echoes
|
|
4
|
+
# A named color theme — foreground / background / cursor / selection
|
|
5
|
+
# plus the 16-entry ANSI palette that drives the 256-color table.
|
|
6
|
+
# Defined in `~/.config/echoes/echoes.conf` via the `profile` DSL:
|
|
7
|
+
#
|
|
8
|
+
# profile "Solarized Dark" do
|
|
9
|
+
# foreground "#93a1a1"
|
|
10
|
+
# background "#002b36"
|
|
11
|
+
# cursor_color "#93a1a1", 0.7
|
|
12
|
+
# selection_color "#586e75"
|
|
13
|
+
# color_palette %w[#073642 #dc322f #859900 #b58900 #268bd2
|
|
14
|
+
# #d33682 #2aa198 #eee8d5 #002b36 #cb4b16
|
|
15
|
+
# #586e75 #657b83 #839496 #6c71c4 #93a1a1 #fdf6e3]
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Attributes left unset inherit from `Echoes.config` so a profile
|
|
19
|
+
# can be a partial override.
|
|
20
|
+
class Profile
|
|
21
|
+
attr_reader :name
|
|
22
|
+
|
|
23
|
+
def initialize(name)
|
|
24
|
+
@name = name.to_s
|
|
25
|
+
@foreground = nil
|
|
26
|
+
@background = nil
|
|
27
|
+
@cursor_color = nil
|
|
28
|
+
@selection_color = nil
|
|
29
|
+
@color_palette = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def foreground(*args)
|
|
33
|
+
args.empty? ? (@foreground || Echoes.config.foreground) : @foreground = parse_color(args)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def background(*args)
|
|
37
|
+
args.empty? ? (@background || Echoes.config.background) : @background = parse_color(args)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cursor_color(*args)
|
|
41
|
+
args.empty? ? (@cursor_color || Echoes.config.cursor_color) : @cursor_color = parse_color(args)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def selection_color(*args)
|
|
45
|
+
args.empty? ? (@selection_color || Echoes.config.selection_color) : @selection_color = parse_color(args)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def color_palette(val = nil)
|
|
49
|
+
if val
|
|
50
|
+
@color_palette = val.map { |c| c.is_a?(String) ? parse_color([c]) : c.map(&:to_f) }
|
|
51
|
+
else
|
|
52
|
+
@color_palette || Echoes.config.color_palette
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def parse_color(args)
|
|
59
|
+
if args.size == 1 && args[0].is_a?(String)
|
|
60
|
+
hex = args[0].delete_prefix('#')
|
|
61
|
+
r = hex[0, 2].to_i(16) / 255.0
|
|
62
|
+
g = hex[2, 2].to_i(16) / 255.0
|
|
63
|
+
b = hex[4, 2].to_i(16) / 255.0
|
|
64
|
+
if hex.size == 8
|
|
65
|
+
a = hex[6, 2].to_i(16) / 255.0
|
|
66
|
+
[r, g, b, a]
|
|
67
|
+
else
|
|
68
|
+
[r, g, b]
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
args.map(&:to_f)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|