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,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Echoes
|
|
6
|
+
# Puts a thin wrapper Echoes.app / EchoesEmbed.app into
|
|
7
|
+
# ~/Applications/ so the user can launch echoes from Spotlight,
|
|
8
|
+
# Dock, or Cmd-Space without digging into the gem's install path.
|
|
9
|
+
# The wrapper's only job is to exec into the *real* launcher
|
|
10
|
+
# inside the gem — the gem's launcher already does all the
|
|
11
|
+
# interesting work (arch forcing on Apple Silicon, Ruby probing,
|
|
12
|
+
# $LOAD_PATH bootstrap).
|
|
13
|
+
#
|
|
14
|
+
# The gem path is baked into the wrapper at install time. After
|
|
15
|
+
# `gem update echoes` the user should re-run `echoes install` to
|
|
16
|
+
# refresh the shortcut to the new gem location.
|
|
17
|
+
module Installer
|
|
18
|
+
USER_APPS_DIR = File.expand_path('~/Applications')
|
|
19
|
+
BUNDLES = %w[Echoes.app EchoesEmbed.app].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# `source_root` is the directory containing the .app bundles —
|
|
24
|
+
# normally the gem's own root, computed from this file's path.
|
|
25
|
+
# Overridable for tests.
|
|
26
|
+
def install(source_root: default_source_root, target_dir: USER_APPS_DIR)
|
|
27
|
+
FileUtils.mkdir_p(target_dir)
|
|
28
|
+
|
|
29
|
+
installed = []
|
|
30
|
+
BUNDLES.each do |bundle|
|
|
31
|
+
source = File.join(source_root, bundle)
|
|
32
|
+
unless Dir.exist?(source)
|
|
33
|
+
warn "echoes install: #{bundle} not found at #{source} (skipping)"
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
target = File.join(target_dir, bundle)
|
|
37
|
+
write_wrapper(source, target)
|
|
38
|
+
installed << target
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if installed.empty?
|
|
42
|
+
puts 'echoes install: nothing to do.'
|
|
43
|
+
else
|
|
44
|
+
puts 'Installed:'
|
|
45
|
+
installed.each { |t| puts " #{t}" }
|
|
46
|
+
puts
|
|
47
|
+
puts 'Re-run `echoes install` after `gem update echoes` to refresh the shortcuts.'
|
|
48
|
+
end
|
|
49
|
+
installed
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def uninstall(target_dir: USER_APPS_DIR)
|
|
53
|
+
removed = []
|
|
54
|
+
BUNDLES.each do |bundle|
|
|
55
|
+
target = File.join(target_dir, bundle)
|
|
56
|
+
next unless Dir.exist?(target)
|
|
57
|
+
FileUtils.remove_entry(target)
|
|
58
|
+
removed << target
|
|
59
|
+
end
|
|
60
|
+
if removed.empty?
|
|
61
|
+
puts 'echoes uninstall: nothing to remove.'
|
|
62
|
+
else
|
|
63
|
+
removed.each { |t| puts "Removed #{t}" }
|
|
64
|
+
end
|
|
65
|
+
removed
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def default_source_root
|
|
69
|
+
# File is at <gem-root>/lib/echoes/installer.rb; two `..`s land
|
|
70
|
+
# at the gem root where Echoes.app / EchoesEmbed.app live.
|
|
71
|
+
File.expand_path('../..', __dir__)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def write_wrapper(source_bundle, target_bundle)
|
|
75
|
+
bundle_name = File.basename(target_bundle)
|
|
76
|
+
executable = bundle_name.delete_suffix('.app')
|
|
77
|
+
|
|
78
|
+
FileUtils.mkdir_p(File.join(target_bundle, 'Contents', 'MacOS'))
|
|
79
|
+
|
|
80
|
+
source_plist = File.join(source_bundle, 'Contents', 'Info.plist')
|
|
81
|
+
if File.exist?(source_plist)
|
|
82
|
+
FileUtils.cp(source_plist, File.join(target_bundle, 'Contents', 'Info.plist'))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
script_path = File.join(target_bundle, 'Contents', 'MacOS', executable)
|
|
86
|
+
File.write(script_path, <<~SHELL)
|
|
87
|
+
#!/bin/bash
|
|
88
|
+
# Auto-generated by `echoes install`. Re-run after `gem update
|
|
89
|
+
# echoes` so this points at the current installed location.
|
|
90
|
+
exec "#{source_bundle}/Contents/MacOS/#{executable}"
|
|
91
|
+
SHELL
|
|
92
|
+
File.chmod(0o755, script_path)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/echoes/objc.rb
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle'
|
|
4
|
+
|
|
5
|
+
module Echoes
|
|
6
|
+
module ObjC
|
|
7
|
+
LIBOBJC = Fiddle.dlopen('/usr/lib/libobjc.A.dylib')
|
|
8
|
+
APPKIT = Fiddle.dlopen('/System/Library/Frameworks/AppKit.framework/AppKit')
|
|
9
|
+
FOUNDATION = Fiddle.dlopen('/System/Library/Frameworks/Foundation.framework/Foundation')
|
|
10
|
+
|
|
11
|
+
# Type aliases
|
|
12
|
+
P = Fiddle::TYPE_VOIDP
|
|
13
|
+
D = Fiddle::TYPE_DOUBLE
|
|
14
|
+
L = Fiddle::TYPE_LONG
|
|
15
|
+
I = Fiddle::TYPE_INT
|
|
16
|
+
V = Fiddle::TYPE_VOID
|
|
17
|
+
|
|
18
|
+
# Core runtime functions
|
|
19
|
+
GetClass = Fiddle::Function.new(LIBOBJC['objc_getClass'], [P], P)
|
|
20
|
+
RegisterName = Fiddle::Function.new(LIBOBJC['sel_registerName'], [P], P)
|
|
21
|
+
AllocateClassPair = Fiddle::Function.new(LIBOBJC['objc_allocateClassPair'], [P, P, I], P)
|
|
22
|
+
AddMethod = Fiddle::Function.new(LIBOBJC['class_addMethod'], [P, P, P, P], I)
|
|
23
|
+
RegisterClassPair = Fiddle::Function.new(LIBOBJC['objc_registerClassPair'], [P], V)
|
|
24
|
+
GetMethodImpl = Fiddle::Function.new(LIBOBJC['class_getMethodImplementation'], [P, P], P)
|
|
25
|
+
AddProtocol = Fiddle::Function.new(LIBOBJC['class_addProtocol'], [P, P], I)
|
|
26
|
+
GetProtocol = Fiddle::Function.new(LIBOBJC['objc_getProtocol'], [P], P)
|
|
27
|
+
|
|
28
|
+
# objc_msgSend variants for different signatures
|
|
29
|
+
def self.new_msg(args, ret)
|
|
30
|
+
Fiddle::Function.new(LIBOBJC['objc_msgSend'], args, ret)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
MSG_PTR = new_msg([P, P], P) # id = msg(id, SEL)
|
|
34
|
+
MSG_PTR_1 = new_msg([P, P, P], P) # id = msg(id, SEL, id)
|
|
35
|
+
MSG_PTR_2 = new_msg([P, P, P, P], P) # id = msg(id, SEL, id, id)
|
|
36
|
+
MSG_PTR_L = new_msg([P, P, L], P) # id = msg(id, SEL, long)
|
|
37
|
+
MSG_PTR_1L = new_msg([P, P, P, L], P) # id = msg(id, SEL, id, long)
|
|
38
|
+
MSG_VOID = new_msg([P, P], V) # void = msg(id, SEL)
|
|
39
|
+
MSG_VOID_1 = new_msg([P, P, P], V) # void = msg(id, SEL, id)
|
|
40
|
+
MSG_VOID_2 = new_msg([P, P, P, P], V) # void = msg(id, SEL, id, id)
|
|
41
|
+
MSG_VOID_4 = new_msg([P, P, P, P, P, P], V) # void = msg(id, SEL, id, id, id, id)
|
|
42
|
+
MSG_PTR_3 = new_msg([P, P, P, P, P], P) # id = msg(id, SEL, id, id, id)
|
|
43
|
+
MSG_VOID_I = new_msg([P, P, I], V) # void = msg(id, SEL, int)
|
|
44
|
+
MSG_VOID_L = new_msg([P, P, L], V) # void = msg(id, SEL, long)
|
|
45
|
+
MSG_VOID_2D = new_msg([P, P, D, D], V) # void = msg(id, SEL, double, double)
|
|
46
|
+
MSG_RET_D = new_msg([P, P], D) # double = msg(id, SEL)
|
|
47
|
+
MSG_PTR_D = new_msg([P, P, D], P) # id = msg(id, SEL, double)
|
|
48
|
+
MSG_RET_D_1 = new_msg([P, P, P], D) # double = msg(id, SEL, id)
|
|
49
|
+
MSG_RET_L = new_msg([P, P], L) # long = msg(id, SEL)
|
|
50
|
+
|
|
51
|
+
# CGRect as 4 doubles
|
|
52
|
+
MSG_PTR_RECT = new_msg([P, P, D, D, D, D], P) # initWithFrame:
|
|
53
|
+
MSG_VOID_RECT = new_msg([P, P, D, D, D, D], V) # NSRectFill equivalent
|
|
54
|
+
MSG_VOID_RECT_1 = new_msg([P, P, D, D, D, D, P], V) # addCursorRect:cursor:
|
|
55
|
+
# NSGradient drawInRect:angle: (4 doubles for rect + 1 double for angle)
|
|
56
|
+
MSG_VOID_RECT_D = new_msg([P, P, D, D, D, D, D], V)
|
|
57
|
+
|
|
58
|
+
# initWithContentRect:styleMask:backing:defer:
|
|
59
|
+
MSG_PTR_RECT_L_L_I = new_msg([P, P, D, D, D, D, L, L, I], P)
|
|
60
|
+
|
|
61
|
+
# drawAtPoint:withAttributes: (NSPoint = 2 doubles + id)
|
|
62
|
+
MSG_VOID_PT_1 = new_msg([P, P, D, D, P], V)
|
|
63
|
+
|
|
64
|
+
# popUpMenuPositioningItem:atLocation:inView: (id + NSPoint + id)
|
|
65
|
+
MSG_VOID_1_PT_1 = new_msg([P, P, P, D, D, P], V)
|
|
66
|
+
|
|
67
|
+
# setDouble:forKey: (double + id) -> void
|
|
68
|
+
MSG_VOID_D_1 = new_msg([P, P, D, P], V)
|
|
69
|
+
|
|
70
|
+
# colorWithRed:green:blue:alpha: (4 doubles)
|
|
71
|
+
MSG_PTR_4D = new_msg([P, P, D, D, D, D], P)
|
|
72
|
+
|
|
73
|
+
# fontWithName:size: (id, double)
|
|
74
|
+
MSG_PTR_1D = new_msg([P, P, P, D], P)
|
|
75
|
+
|
|
76
|
+
# monospacedSystemFontOfSize:weight: (2 doubles)
|
|
77
|
+
MSG_PTR_2D = new_msg([P, P, D, D], P)
|
|
78
|
+
|
|
79
|
+
# scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
|
|
80
|
+
MSG_PTR_D_P_P_P_I = new_msg([P, P, D, P, P, P, I], P)
|
|
81
|
+
|
|
82
|
+
# NSRectFill C function
|
|
83
|
+
NSRectFill = Fiddle::Function.new(APPKIT['NSRectFill'], [D, D, D, D], V)
|
|
84
|
+
|
|
85
|
+
# Cocoa constants
|
|
86
|
+
NSWindowStyleMaskTitled = 1 << 0
|
|
87
|
+
NSWindowStyleMaskClosable = 1 << 1
|
|
88
|
+
NSWindowStyleMaskMiniaturizable = 1 << 2
|
|
89
|
+
NSWindowStyleMaskResizable = 1 << 3
|
|
90
|
+
NSWindowStyleMaskDefault = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
|
91
|
+
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
|
|
92
|
+
NSBackingStoreBuffered = 2
|
|
93
|
+
|
|
94
|
+
NSEventModifierFlagShift = 1 << 17
|
|
95
|
+
NSEventModifierFlagControl = 1 << 18
|
|
96
|
+
NSEventModifierFlagOption = 1 << 19
|
|
97
|
+
NSEventModifierFlagCommand = 1 << 20
|
|
98
|
+
NSEventModifierFlagNumericPad = 1 << 21
|
|
99
|
+
|
|
100
|
+
# Selector cache
|
|
101
|
+
SEL_CACHE = {}
|
|
102
|
+
|
|
103
|
+
def self.cls(name)
|
|
104
|
+
GetClass.call(name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.sel(name)
|
|
108
|
+
SEL_CACHE[name] ||= RegisterName.call(name)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.retain(obj)
|
|
112
|
+
MSG_PTR.call(obj, sel('retain'))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.release(obj)
|
|
116
|
+
MSG_VOID.call(obj, sel('release'))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.nsstring(str)
|
|
120
|
+
MSG_PTR_1.call(cls('NSString'), sel('stringWithUTF8String:'), str)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.to_ruby_string(nsstring_ptr)
|
|
124
|
+
cstr = MSG_PTR.call(nsstring_ptr, sel('UTF8String'))
|
|
125
|
+
cstr.to_s.force_encoding(Encoding::UTF_8)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.nsdict(hash)
|
|
129
|
+
dict = MSG_PTR.call(cls('NSMutableDictionary'), sel('dictionary'))
|
|
130
|
+
hash.each do |key, value|
|
|
131
|
+
MSG_VOID_2.call(dict, sel('setObject:forKey:'), value, key)
|
|
132
|
+
end
|
|
133
|
+
dict
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.nsnumber_int(val)
|
|
137
|
+
MSG_PTR_L.call(cls('NSNumber'), sel('numberWithInteger:'), val)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.define_class(name, superclass_name, methods)
|
|
141
|
+
super_cls = cls(superclass_name)
|
|
142
|
+
new_cls = AllocateClassPair.call(super_cls, name, 0)
|
|
143
|
+
methods.each do |sel_name, (type_encoding, closure)|
|
|
144
|
+
AddMethod.call(new_cls, sel(sel_name), closure, type_encoding)
|
|
145
|
+
end
|
|
146
|
+
RegisterClassPair.call(new_cls)
|
|
147
|
+
new_cls
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# AppKit string constant accessors
|
|
151
|
+
def self.appkit_const(name)
|
|
152
|
+
ptr = Fiddle::Pointer.new(APPKIT[name])
|
|
153
|
+
Fiddle::Pointer.new(ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
NSFontAttributeName = appkit_const('NSFontAttributeName')
|
|
157
|
+
NSForegroundColorAttributeName = appkit_const('NSForegroundColorAttributeName')
|
|
158
|
+
NSUnderlineStyleAttributeName = appkit_const('NSUnderlineStyleAttributeName')
|
|
159
|
+
NSStrikethroughStyleAttributeName = appkit_const('NSStrikethroughStyleAttributeName')
|
|
160
|
+
NSPasteboardTypeString = appkit_const('NSPasteboardTypeString')
|
|
161
|
+
NSPasteboardTypeFileURL = appkit_const('NSPasteboardTypeFileURL')
|
|
162
|
+
|
|
163
|
+
# CoreGraphics framework
|
|
164
|
+
COREGRAPHICS = Fiddle.dlopen('/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics')
|
|
165
|
+
|
|
166
|
+
CGColorSpaceCreateDeviceRGB = Fiddle::Function.new(COREGRAPHICS['CGColorSpaceCreateDeviceRGB'], [], P)
|
|
167
|
+
CGColorSpaceRelease = Fiddle::Function.new(COREGRAPHICS['CGColorSpaceRelease'], [P], V)
|
|
168
|
+
CGBitmapContextCreate = Fiddle::Function.new(COREGRAPHICS['CGBitmapContextCreate'], [P, L, L, L, L, P, I], P)
|
|
169
|
+
CGBitmapContextCreateImage = Fiddle::Function.new(COREGRAPHICS['CGBitmapContextCreateImage'], [P], P)
|
|
170
|
+
CGContextDrawImage = Fiddle::Function.new(COREGRAPHICS['CGContextDrawImage'], [P, D, D, D, D, P], V)
|
|
171
|
+
CGContextSaveGState = Fiddle::Function.new(COREGRAPHICS['CGContextSaveGState'], [P], V)
|
|
172
|
+
CGContextRestoreGState = Fiddle::Function.new(COREGRAPHICS['CGContextRestoreGState'], [P], V)
|
|
173
|
+
CGContextTranslateCTM = Fiddle::Function.new(COREGRAPHICS['CGContextTranslateCTM'], [P, D, D], V)
|
|
174
|
+
CGContextScaleCTM = Fiddle::Function.new(COREGRAPHICS['CGContextScaleCTM'], [P, D, D], V)
|
|
175
|
+
CGImageRelease = Fiddle::Function.new(COREGRAPHICS['CGImageRelease'], [P], V)
|
|
176
|
+
CGContextRelease = Fiddle::Function.new(COREGRAPHICS['CGContextRelease'], [P], V)
|
|
177
|
+
|
|
178
|
+
# kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault
|
|
179
|
+
KCGImageAlphaPremultipliedLast = 1
|
|
180
|
+
|
|
181
|
+
# CoreText framework
|
|
182
|
+
CORETEXT = Fiddle.dlopen('/System/Library/Frameworks/CoreText.framework/CoreText')
|
|
183
|
+
|
|
184
|
+
# CTFontCreateForString(CTFontRef currentFont, CFStringRef string, CFRange range) -> CTFontRef
|
|
185
|
+
# CFRange is {CFIndex, CFIndex} = {long, long}, decomposed into 2 GPR args on arm64
|
|
186
|
+
CTFontCreateForString = Fiddle::Function.new(CORETEXT['CTFontCreateForString'], [P, P, L, L], P)
|
|
187
|
+
end
|
|
188
|
+
end
|