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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 840cfd55fa04459db94048feb0ace4ac1fcd24afe1be90782f90ad10ee849c3f
|
|
4
|
+
data.tar.gz: 93f77b0d7360472791d898ceef10049a72019ef3facacbe73280648f740eb929
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68c56c9b24cf3bd2456cea2b559bff0d7e4a25df9562561c6d29b79b0a9f54b171074f5dcc6cbf0731336037821ddd641f95295f1bfc81ae158e080089be2210
|
|
7
|
+
data.tar.gz: 9f86318197b4796b239013d626fdbf0e2214a5fa343c215c56518fdd77f25bce3c04995929840b496dbb1d8fa61bd3c769db4eb794ea45a7d47fa6808c97b1ca
|
data/lib/echoes/client.rb
CHANGED
|
@@ -62,6 +62,20 @@ module Echoes
|
|
|
62
62
|
nil
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Ask Echoes to write the current pane to `path`. Format is
|
|
66
|
+
# picked from the file extension: `.png` produces a rasterized
|
|
67
|
+
# PNG; anything else (including `.pdf`) produces a vector PDF
|
|
68
|
+
# via [NSView dataWithPDFInsideRect:] — typically much smaller
|
|
69
|
+
# than the PNG equivalent for terminal content. The path should
|
|
70
|
+
# be absolute. There's no reply on the wire; the caller polls
|
|
71
|
+
# the filesystem. Other terminals ignore the OSC, so the call
|
|
72
|
+
# is a no-op outside Echoes (no file gets written).
|
|
73
|
+
def capture(path, io: $stdout)
|
|
74
|
+
io.write("#{OSC};capture;#{path}#{BEL}")
|
|
75
|
+
io.flush if io.respond_to?(:flush)
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
65
79
|
# Drop any pane background override (bg-color/bg-gradient) AND
|
|
66
80
|
# all bg-fill overlays. Safe to call when nothing is set.
|
|
67
81
|
def bg_clear(io: $stdout)
|
|
@@ -70,15 +84,18 @@ module Echoes
|
|
|
70
84
|
nil
|
|
71
85
|
end
|
|
72
86
|
|
|
73
|
-
# Emit
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
87
|
+
# Emit scaled / aligned multicell text. Routes through OSC 66
|
|
88
|
+
# (the kitty-compatible spec, portable across terminals) when
|
|
89
|
+
# only standard knobs are used, and through OSC 7772 ;multicell
|
|
90
|
+
# (Echoes-private) when an extension param like `family:` is
|
|
91
|
+
# set. On unknown families, Echoes falls back to the monospaced
|
|
92
|
+
# system font; non-Echoes terminals ignore the whole OSC 7772
|
|
93
|
+
# frame, so the text just doesn't render — same trade as any
|
|
94
|
+
# private OSC.
|
|
78
95
|
#
|
|
79
96
|
# Examples:
|
|
80
|
-
# Echoes::Client.styled_text("Title",
|
|
81
|
-
# Echoes::Client.styled_text("
|
|
97
|
+
# Echoes::Client.styled_text("Title", scale: 3) # OSC 66, portable
|
|
98
|
+
# Echoes::Client.styled_text("Title", scale: 3, family: "Helvetica Neue") # OSC 7772, Echoes-only
|
|
82
99
|
def styled_text(text, scale: 1, width: nil, frac_n: nil, frac_d: nil,
|
|
83
100
|
valign: nil, halign: nil, family: nil, io: $stdout)
|
|
84
101
|
meta = +"s=#{scale}"
|
|
@@ -87,8 +104,12 @@ module Echoes
|
|
|
87
104
|
meta << ":d=#{frac_d}" if frac_d
|
|
88
105
|
meta << ":v=#{valign}" if valign
|
|
89
106
|
meta << ":h=#{halign}" if halign
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
if family
|
|
108
|
+
meta << ":f=#{family}"
|
|
109
|
+
io.write("\e]7772;multicell;#{meta};#{text}\a")
|
|
110
|
+
else
|
|
111
|
+
io.write("\e]66;#{meta};#{text}\a")
|
|
112
|
+
end
|
|
92
113
|
io.flush if io.respond_to?(:flush)
|
|
93
114
|
nil
|
|
94
115
|
end
|
data/lib/echoes/configuration.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'profile'
|
|
4
|
+
|
|
3
5
|
module Echoes
|
|
4
6
|
class Configuration
|
|
5
7
|
def initialize
|
|
@@ -21,6 +23,36 @@ module Echoes
|
|
|
21
23
|
@pane_divider_color = [0.4, 0.4, 0.4]
|
|
22
24
|
@active_pane_border_color = [0.3, 0.5, 0.8]
|
|
23
25
|
@copy_mode_cursor_color = [0.8, 0.7, 0.2]
|
|
26
|
+
register_default_profiles
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Two opinionated-but-recognizable profiles ship with Echoes so
|
|
30
|
+
# the View → Profile submenu has alternatives the moment the
|
|
31
|
+
# user opens it, without forcing them to write any DSL. Users
|
|
32
|
+
# can declare more (or override these) in their config file —
|
|
33
|
+
# `profile "Solarized Dark" do …` redeclaring an existing name
|
|
34
|
+
# replaces the entry.
|
|
35
|
+
def register_default_profiles
|
|
36
|
+
profile "Solarized Dark" do
|
|
37
|
+
foreground "#93a1a1"
|
|
38
|
+
background "#002b36"
|
|
39
|
+
cursor_color "#93a1a1"
|
|
40
|
+
selection_color "#586e75"
|
|
41
|
+
color_palette %w[
|
|
42
|
+
#073642 #dc322f #859900 #b58900 #268bd2 #d33682 #2aa198 #eee8d5
|
|
43
|
+
#002b36 #cb4b16 #586e75 #657b83 #839496 #6c71c4 #93a1a1 #fdf6e3
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
profile "Solarized Light" do
|
|
47
|
+
foreground "#586e75"
|
|
48
|
+
background "#fdf6e3"
|
|
49
|
+
cursor_color "#586e75"
|
|
50
|
+
selection_color "#93a1a1"
|
|
51
|
+
color_palette %w[
|
|
52
|
+
#073642 #dc322f #859900 #b58900 #268bd2 #d33682 #2aa198 #eee8d5
|
|
53
|
+
#002b36 #cb4b16 #586e75 #657b83 #839496 #6c71c4 #93a1a1 #fdf6e3
|
|
54
|
+
]
|
|
55
|
+
end
|
|
24
56
|
end
|
|
25
57
|
|
|
26
58
|
def font_family(val = nil)
|
|
@@ -99,6 +131,105 @@ module Echoes
|
|
|
99
131
|
end
|
|
100
132
|
end
|
|
101
133
|
|
|
134
|
+
# Define a named profile (color theme). Anything not set inside
|
|
135
|
+
# the block falls back to the top-level config attrs, so a profile
|
|
136
|
+
# is a partial override.
|
|
137
|
+
#
|
|
138
|
+
# profile "Light" do
|
|
139
|
+
# foreground "#1a1a1a"
|
|
140
|
+
# background "#ffffff"
|
|
141
|
+
# end
|
|
142
|
+
def profile(name, &block)
|
|
143
|
+
p = Profile.new(name)
|
|
144
|
+
p.instance_eval(&block) if block
|
|
145
|
+
profiles[p.name] = p
|
|
146
|
+
p
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def profiles
|
|
150
|
+
@profiles ||= {}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Profiles plus the synthesized "Default" — the menu / GUI uses
|
|
154
|
+
# this so the View → Profile submenu always has at least a
|
|
155
|
+
# discoverable "Default" entry even when the user hasn't
|
|
156
|
+
# declared any profiles.
|
|
157
|
+
def all_profiles
|
|
158
|
+
base = {'Default' => synthesized_profile}
|
|
159
|
+
base.merge(profiles)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Rebind a menu shortcut. The first arg is a shortcut string
|
|
163
|
+
# like "Cmd+Shift+S" (Cmd / Shift / Ctrl / Option modifiers,
|
|
164
|
+
# case-insensitive); pass an empty string to disable the
|
|
165
|
+
# shortcut entirely. The second is the action symbol — one
|
|
166
|
+
# of the symbols Echoes documents (:new_window, :split_right,
|
|
167
|
+
# :toggle_find, etc.).
|
|
168
|
+
#
|
|
169
|
+
# keybind "Cmd+Shift+T", :new_tab
|
|
170
|
+
# keybind "", :toggle_pointer # disable
|
|
171
|
+
def keybind(shortcut, action)
|
|
172
|
+
keybinds[action.to_sym] = parse_shortcut(shortcut.to_s)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def keybinds
|
|
176
|
+
@keybinds ||= {}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Returns {key:, modifiers:} or nil if no override is set.
|
|
180
|
+
def keybind_for(action)
|
|
181
|
+
keybinds[action.to_sym]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Matches the NSEvent modifier flag bits — kept in sync with
|
|
185
|
+
# `ObjC::NSEventModifierFlag*` in objc.rb. Defined here so
|
|
186
|
+
# Configuration can resolve shortcut strings without depending
|
|
187
|
+
# on the AppKit-loading objc.rb at config-load time.
|
|
188
|
+
SHORTCUT_MODIFIERS = {
|
|
189
|
+
'shift' => 1 << 17,
|
|
190
|
+
'control' => 1 << 18,
|
|
191
|
+
'ctrl' => 1 << 18,
|
|
192
|
+
'option' => 1 << 19,
|
|
193
|
+
'opt' => 1 << 19,
|
|
194
|
+
'alt' => 1 << 19,
|
|
195
|
+
'cmd' => 1 << 20,
|
|
196
|
+
'command' => 1 << 20,
|
|
197
|
+
'super' => 1 << 20,
|
|
198
|
+
}.freeze
|
|
199
|
+
|
|
200
|
+
def parse_shortcut(str)
|
|
201
|
+
return {key: '', modifiers: 0} if str.nil? || str.empty?
|
|
202
|
+
parts = str.split('+').map(&:strip)
|
|
203
|
+
key = parts.pop.to_s.downcase
|
|
204
|
+
modifiers = parts.sum { |p| SHORTCUT_MODIFIERS[p.downcase] || 0 }
|
|
205
|
+
{key: key, modifiers: modifiers}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Pick / look up the active profile by name. With no arg, returns
|
|
209
|
+
# whatever the user named via `default_profile "Foo"`, falling
|
|
210
|
+
# back to the synthesized "Default" so legacy configs (just
|
|
211
|
+
# foreground/background/etc.) keep working unchanged.
|
|
212
|
+
def default_profile(name = nil)
|
|
213
|
+
if name
|
|
214
|
+
@default_profile_name = name.to_s
|
|
215
|
+
else
|
|
216
|
+
profiles[@default_profile_name] || synthesized_profile
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Build a "Default" Profile from the flat config attrs every
|
|
221
|
+
# time it's asked for — never memoized, because the user's
|
|
222
|
+
# config might mutate the flat attrs after our first call.
|
|
223
|
+
def synthesized_profile
|
|
224
|
+
p = Profile.new('Default')
|
|
225
|
+
p.instance_variable_set(:@foreground, @foreground)
|
|
226
|
+
p.instance_variable_set(:@background, @background)
|
|
227
|
+
p.instance_variable_set(:@cursor_color, @cursor_color)
|
|
228
|
+
p.instance_variable_set(:@selection_color, @selection_color)
|
|
229
|
+
p.instance_variable_set(:@color_palette, @color_palette)
|
|
230
|
+
p
|
|
231
|
+
end
|
|
232
|
+
|
|
102
233
|
private
|
|
103
234
|
|
|
104
235
|
def parse_color(args)
|
|
@@ -40,6 +40,11 @@ module Echoes
|
|
|
40
40
|
def initialize(no_rc: false)
|
|
41
41
|
ENV['GIT_PAGER'] ||= 'cat'
|
|
42
42
|
ENV['PAGER'] ||= 'cat'
|
|
43
|
+
# Default $TERM so terminfo-aware tools (tput, less, vim, …)
|
|
44
|
+
# have a profile to look up. CI runners launch the rake task
|
|
45
|
+
# without TERM set; programs that read it bail with
|
|
46
|
+
# "No value for $TERM and no -T specified".
|
|
47
|
+
ENV['TERM'] ||= Echoes.config.term
|
|
43
48
|
|
|
44
49
|
helper_env = {'ECHOES_HELPER_NO_RC' => no_rc ? '1' : nil}
|
|
45
50
|
@master, slave = PTY.open
|
|
@@ -119,9 +124,9 @@ module Echoes
|
|
|
119
124
|
# through the pty asynchronously and `command_done` will set
|
|
120
125
|
# @running back to false. The line is added to history (rubish-side)
|
|
121
126
|
# by the helper before execution.
|
|
122
|
-
def submit_line(line, rows: 24, cols: 80)
|
|
127
|
+
def submit_line(line, rows: 24, cols: 80, px_width: 0, px_height: 0)
|
|
123
128
|
return if running?
|
|
124
|
-
@master.winsize = [rows, cols] rescue nil
|
|
129
|
+
@master.winsize = [rows, cols, px_width.to_i, px_height.to_i] rescue nil
|
|
125
130
|
@running = true
|
|
126
131
|
rpc_async('execute', line: line)
|
|
127
132
|
end
|
|
@@ -194,8 +199,8 @@ module Echoes
|
|
|
194
199
|
# Updates the pty master's winsize. The kernel propagates to the
|
|
195
200
|
# slave (which is the helper's ctty) and emits SIGWINCH to the
|
|
196
201
|
# foreground process group, so vim/less/etc. repaint.
|
|
197
|
-
def resize(rows:, cols:)
|
|
198
|
-
@master.winsize = [rows, cols]
|
|
202
|
+
def resize(rows:, cols:, px_width: 0, px_height: 0)
|
|
203
|
+
@master.winsize = [rows, cols, px_width.to_i, px_height.to_i]
|
|
199
204
|
rescue IOError, Errno::EIO
|
|
200
205
|
end
|
|
201
206
|
|