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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c05df4a1ad800e1b7e982aa2577a2bd6c64bfe15ec4c630aff8a561b53992072
4
- data.tar.gz: dc78e9e8813c237375818a76e87b3a756cf9431e0dfb78da331cb5e8b309ca5a
3
+ metadata.gz: 840cfd55fa04459db94048feb0ace4ac1fcd24afe1be90782f90ad10ee849c3f
4
+ data.tar.gz: 93f77b0d7360472791d898ceef10049a72019ef3facacbe73280648f740eb929
5
5
  SHA512:
6
- metadata.gz: 8fd75f4eff8db332e59669df7359c0a0a904d9b879da99dcd33aa4dfb053a28869f80e556b9f783c6e362b8b34397ffab264d59a0aeda9c76244ad639d187dfa
7
- data.tar.gz: 53a1aa9d6ee94264a31fb68c9f94ff5289415540e48461031a9b91cf52b31f8706bd9fce89dee880b2d1546c94b25605c4955545fefa0475b5aeb4c2908b8920
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 text via OSC 66 (multicell), with optional cell-scale,
74
- # sub-cell fraction, vertical/horizontal alignment, and font
75
- # family. `family:` is an Echoes-specific extension other
76
- # terminals ignore. On unknown families, Echoes falls back to the
77
- # monospaced system font.
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", scale: 3, family: "Helvetica Neue")
81
- # Echoes::Client.styled_text("• item", scale: 1, family: "Menlo")
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
- meta << ":f=#{family}" if family
91
- io.write("\e]66;#{meta};#{text}\a")
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
@@ -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