echoes 0.5.0 → 0.6.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/README.md +6 -3
- data/lib/echoes/gui.rb +96 -6
- data/lib/echoes/objc.rb +3 -0
- data/lib/echoes/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5125e5e42d6d0a336df431a944a75f07ba2b90ba91a3996f0a9fe9e634878c62
|
|
4
|
+
data.tar.gz: 7f1b2ea05ad61fe36922eef9da79fa04db3446b0019b2a25d2b0a5d4ad555205
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d22756dcafcec61e3d73a87ce3788bd928edea5eb3a3eb137ec6481da5d1faeda999b86850c1ae34f826c8eb57d876f1cebfb560ceacd85c49a3716bea98a1a3
|
|
7
|
+
data.tar.gz: 219539e6e1b55c53ce7690740b3f891a41034dd1df3275d8a8db7bcc2b6739f637236e9484a415d5e82e12a89b1c54d82251148f6faad130d09bc91caadd94f2
|
data/README.md
CHANGED
|
@@ -96,6 +96,8 @@ bundle exec exe/echoes -t # TTY mode
|
|
|
96
96
|
| Cmd+Shift+D | Split pane down |
|
|
97
97
|
| Cmd+] / Cmd+[ | Next / previous pane |
|
|
98
98
|
| Cmd+Shift+] / Cmd+Shift+[ | Next / previous tab |
|
|
99
|
+
| Cmd+1 … Cmd+8 | Jump to tab 1 … 8 |
|
|
100
|
+
| Cmd+9 | Jump to the last tab |
|
|
99
101
|
| Cmd++ / Cmd+- / Cmd+0 | Bigger / smaller / reset font |
|
|
100
102
|
| Cmd+F | Find |
|
|
101
103
|
| Cmd+G / Cmd+Shift+G | Find next / previous |
|
|
@@ -161,9 +163,10 @@ keybind "", :toggle_pointer # disable the default
|
|
|
161
163
|
Available actions (one per menu item): `:new_window`, `:new_tab`,
|
|
162
164
|
`:close_tab`, `:close_pane`, `:edit_file`, `:split_right`, `:split_down`,
|
|
163
165
|
`:select_next_pane`, `:select_previous_pane`, `:show_next_tab`,
|
|
164
|
-
`:show_previous_tab`, `:
|
|
165
|
-
|
|
166
|
-
`:
|
|
166
|
+
`:show_previous_tab`, `:select_tab_1` … `:select_tab_9`
|
|
167
|
+
(jump-to-tab; `:select_tab_9` is "last tab"), `:increase_font_size`,
|
|
168
|
+
`:decrease_font_size`, `:reset_font_size`, `:toggle_find`,
|
|
169
|
+
`:find_next`, `:find_previous`, `:toggle_pointer`, `:toggle_copy_mode`.
|
|
167
170
|
|
|
168
171
|
Modifier names are case-insensitive and accept the obvious aliases:
|
|
169
172
|
`Cmd`/`Command`/`Super`, `Ctrl`/`Control`, `Opt`/`Option`/`Alt`, `Shift`.
|
data/lib/echoes/gui.rb
CHANGED
|
@@ -143,6 +143,20 @@ module Echoes
|
|
|
143
143
|
@tabs[@active_tab]
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
+
# Switch to a tab by 1-based slot number (Cmd+N shortcuts).
|
|
147
|
+
# n=1..8 maps to that tab index; n=9 maps to the LAST tab
|
|
148
|
+
# regardless of count, matching Safari / Chrome / iTerm2 /
|
|
149
|
+
# Ghostty. Returns true when the active tab actually changed
|
|
150
|
+
# (the caller redraws on true), false when the request was a
|
|
151
|
+
# no-op — target out of range or already active.
|
|
152
|
+
def select_tab(n)
|
|
153
|
+
target = (n == 9) ? @tabs.size - 1 : n - 1
|
|
154
|
+
return false if target < 0 || target >= @tabs.size
|
|
155
|
+
return false if target == @active_tab
|
|
156
|
+
@active_tab = target
|
|
157
|
+
true
|
|
158
|
+
end
|
|
159
|
+
|
|
146
160
|
# Phase 1 launch flag for the in-process Rubish embedding. Setting
|
|
147
161
|
# ECHOES_EMBED=1 in the environment routes new Tab/Pane creation
|
|
148
162
|
# through Echoes::EmbeddedShell instead of PTY.spawn.
|
|
@@ -323,6 +337,12 @@ module Echoes
|
|
|
323
337
|
add_menu_item(window_menu, "Show Next Tab", 'showNextTab:', '}',
|
|
324
338
|
modifiers: ObjC::NSEventModifierFlagCommand | ObjC::NSEventModifierFlagShift,
|
|
325
339
|
bind: :show_next_tab)
|
|
340
|
+
# Cmd+1..8 jump to that tab index; Cmd+9 goes to the last tab.
|
|
341
|
+
(1..9).each do |n|
|
|
342
|
+
title = (n == 9) ? "Show Last Tab" : "Show Tab #{n}"
|
|
343
|
+
add_menu_item(window_menu, title, "selectTab#{n}:", n.to_s,
|
|
344
|
+
bind: :"select_tab_#{n}")
|
|
345
|
+
end
|
|
326
346
|
add_separator(window_menu)
|
|
327
347
|
add_menu_item(window_menu, "Select Next Pane", 'selectNextPane:', ']', bind: :select_next_pane)
|
|
328
348
|
add_menu_item(window_menu, "Select Previous Pane", 'selectPreviousPane:', '[', bind: :select_previous_pane)
|
|
@@ -435,10 +455,19 @@ module Echoes
|
|
|
435
455
|
def create_fonts
|
|
436
456
|
@font = ObjC.retain(create_nsfont(@font_size))
|
|
437
457
|
@bold_font = ObjC.retain(create_bold_nsfont(@font))
|
|
458
|
+
@tab_font = ObjC.retain(create_nsfont(tab_font_size))
|
|
438
459
|
@font_y_offset_cache = {}
|
|
439
460
|
update_cell_metrics
|
|
440
461
|
end
|
|
441
462
|
|
|
463
|
+
# Tab labels render in a smaller font than the cell grid so long
|
|
464
|
+
# titles read comfortably even when the bar's split across many
|
|
465
|
+
# narrow tabs. Scales with the user's font_size so the bar stays
|
|
466
|
+
# proportional at any zoom.
|
|
467
|
+
def tab_font_size
|
|
468
|
+
[(@font_size * 0.85).round(1), 9.0].max
|
|
469
|
+
end
|
|
470
|
+
|
|
442
471
|
def create_view_class
|
|
443
472
|
gui = self
|
|
444
473
|
|
|
@@ -770,6 +799,15 @@ module Echoes
|
|
|
770
799
|
@active_tab = (@active_tab + 1) % @tabs.size
|
|
771
800
|
ObjC::MSG_VOID_I.call(@view, ObjC.sel('setNeedsDisplay:'), 1)
|
|
772
801
|
})
|
|
802
|
+
# Cmd+1..8 → tabs 1..8; Cmd+9 → last tab (whichever it is) —
|
|
803
|
+
# matches Safari, Chrome, iTerm2, Ghostty. The indexing /
|
|
804
|
+
# bounds / no-op logic lives in select_tab so it's testable
|
|
805
|
+
# without standing up the closure + menu machinery.
|
|
806
|
+
@select_tab_closures = (1..9).map do |n|
|
|
807
|
+
menu_action.call(-> {
|
|
808
|
+
ObjC::MSG_VOID_I.call(@view, ObjC.sel('setNeedsDisplay:'), 1) if select_tab(n)
|
|
809
|
+
})
|
|
810
|
+
end
|
|
773
811
|
@split_right_closure = menu_action.call(-> {
|
|
774
812
|
tab = current_tab
|
|
775
813
|
new_pane = tab.split_vertical
|
|
@@ -902,6 +940,9 @@ module Echoes
|
|
|
902
940
|
'findPrevious:' => ['v@:@', @find_prev_closure],
|
|
903
941
|
'showPreviousTab:' => ['v@:@', @prev_tab_closure],
|
|
904
942
|
'showNextTab:' => ['v@:@', @next_tab_closure],
|
|
943
|
+
**(1..9).each_with_object({}) { |n, h|
|
|
944
|
+
h["selectTab#{n}:"] = ['v@:@', @select_tab_closures[n - 1]]
|
|
945
|
+
},
|
|
905
946
|
'splitRight:' => ['v@:@', @split_right_closure],
|
|
906
947
|
'splitDown:' => ['v@:@', @split_down_closure],
|
|
907
948
|
'closePane:' => ['v@:@', @close_pane_closure],
|
|
@@ -2084,10 +2125,13 @@ module Echoes
|
|
|
2084
2125
|
Preferences.set_double(:font_size, new_size) if persist
|
|
2085
2126
|
old_font = @font
|
|
2086
2127
|
old_bold = @bold_font
|
|
2128
|
+
old_tab = @tab_font
|
|
2087
2129
|
@font = ObjC.retain(create_nsfont(@font_size))
|
|
2088
2130
|
@bold_font = ObjC.retain(create_bold_nsfont(@font))
|
|
2131
|
+
@tab_font = ObjC.retain(create_nsfont(tab_font_size))
|
|
2089
2132
|
ObjC.release(old_font) if old_font
|
|
2090
2133
|
ObjC.release(old_bold) if old_bold
|
|
2134
|
+
ObjC.release(old_tab) if old_tab
|
|
2091
2135
|
@font_cache.each_value { |f| ObjC.release(f) unless f.to_i == old_font&.to_i }
|
|
2092
2136
|
@font_cache = {}
|
|
2093
2137
|
@font_y_offset_cache = {}
|
|
@@ -2522,6 +2566,20 @@ module Echoes
|
|
|
2522
2566
|
ObjC::CGContextRestoreGState.call(cg_ctx)
|
|
2523
2567
|
end
|
|
2524
2568
|
|
|
2569
|
+
NSLineBreakByTruncatingTail = 4
|
|
2570
|
+
# macOS empirically uses { Left=0, Center=1, Right=2 } at runtime
|
|
2571
|
+
# (despite Apple's header comments and several years of doc pages
|
|
2572
|
+
# claiming Center=2 / Right=1 for macOS) — verified by setting and
|
|
2573
|
+
# rendering with each value against an offscreen bitmap context.
|
|
2574
|
+
# Get this wrong and the truncated tab title pins to the right edge.
|
|
2575
|
+
NSTextAlignmentCenter = 1
|
|
2576
|
+
# Engage the modern Cocoa typesetter so the paragraph-style
|
|
2577
|
+
# alignment + line-break-mode actually take effect; the simple
|
|
2578
|
+
# NSString-based draw APIs honor font/color but silently ignore
|
|
2579
|
+
# paragraph styles.
|
|
2580
|
+
NSStringDrawingUsesLineFragmentOrigin = 1
|
|
2581
|
+
NSStringDrawingTruncatesLastVisibleLine = 1 << 5 # 32
|
|
2582
|
+
|
|
2525
2583
|
def draw_tab_bar(tbh, ty)
|
|
2526
2584
|
total_w = @cell_width * @cols
|
|
2527
2585
|
tab_w = total_w / @tabs.size
|
|
@@ -2531,24 +2589,51 @@ module Echoes
|
|
|
2531
2589
|
ObjC::NSRectFill.call(0.0, ty, total_w + @cell_width, tbh)
|
|
2532
2590
|
|
|
2533
2591
|
# Vertically center titles in the bar.
|
|
2534
|
-
title_y = ty + (tbh - @
|
|
2592
|
+
title_y = ty + (tbh - @tab_font_line_height) / 2.0
|
|
2535
2593
|
|
|
2536
2594
|
accent_color = make_color(*@active_profile.cursor_color)
|
|
2537
2595
|
accent_h = 2.0
|
|
2538
2596
|
accent_inset = @cell_width * 0.5
|
|
2539
2597
|
|
|
2598
|
+
# Truncating-tail paragraph style so titles wider than the tab
|
|
2599
|
+
# render with a trailing "…" instead of overlapping their
|
|
2600
|
+
# neighbors. Built once per draw (paragraph style is mutable
|
|
2601
|
+
# and shared across the loop's attrs dicts).
|
|
2602
|
+
ns_para = ObjC::MSG_PTR.call(ObjC.cls('NSMutableParagraphStyle'), ObjC.sel('alloc'))
|
|
2603
|
+
ns_para = ObjC::MSG_PTR.call(ns_para, ObjC.sel('init'))
|
|
2604
|
+
ObjC::MSG_VOID_L.call(ns_para, ObjC.sel('setLineBreakMode:'), NSLineBreakByTruncatingTail)
|
|
2605
|
+
ObjC::MSG_VOID_L.call(ns_para, ObjC.sel('setAlignment:'), NSTextAlignmentCenter)
|
|
2606
|
+
|
|
2607
|
+
pad = @cell_width * 0.5
|
|
2608
|
+
rect_w = tab_w - 2 * pad
|
|
2609
|
+
rect_w = 0.0 if rect_w < 0
|
|
2610
|
+
|
|
2540
2611
|
@tabs.each_with_index do |tab, i|
|
|
2541
2612
|
x = i * tab_w
|
|
2542
2613
|
is_active = (i == @active_tab)
|
|
2543
2614
|
|
|
2544
|
-
|
|
2545
|
-
ns_label = ObjC.nsstring(label)
|
|
2615
|
+
ns_label = ObjC.nsstring(tab.title)
|
|
2546
2616
|
ns_attrs = ObjC.nsdict({
|
|
2547
|
-
ObjC::NSFontAttributeName => @
|
|
2617
|
+
ObjC::NSFontAttributeName => @tab_font,
|
|
2548
2618
|
ObjC::NSForegroundColorAttributeName => is_active ? @tab_fg_active : @tab_fg_inactive,
|
|
2619
|
+
ObjC::NSParagraphStyleAttributeName => ns_para,
|
|
2549
2620
|
})
|
|
2550
|
-
|
|
2551
|
-
|
|
2621
|
+
# Build an NSAttributedString rather than passing the dict to
|
|
2622
|
+
# NSString's draw methods — the NSString-based draw APIs honor
|
|
2623
|
+
# font + color but silently ignore paragraph-style alignment,
|
|
2624
|
+
# which is why earlier attempts left the truncated text
|
|
2625
|
+
# drifted toward one edge instead of centered. NSAttributedString
|
|
2626
|
+
# drawWithRect:options:context: routes through the canonical
|
|
2627
|
+
# typesetter and respects the alignment + truncating-tail mode.
|
|
2628
|
+
attr_str = ObjC::MSG_PTR.call(ObjC.cls('NSAttributedString'), ObjC.sel('alloc'))
|
|
2629
|
+
attr_str = ObjC::MSG_PTR_2.call(attr_str,
|
|
2630
|
+
ObjC.sel('initWithString:attributes:'), ns_label, ns_attrs)
|
|
2631
|
+
ObjC::MSG_VOID_RECT_L_1.call(attr_str,
|
|
2632
|
+
ObjC.sel('drawWithRect:options:context:'),
|
|
2633
|
+
x + pad, title_y, rect_w, @tab_font_line_height,
|
|
2634
|
+
NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine,
|
|
2635
|
+
Fiddle::Pointer.new(0))
|
|
2636
|
+
ObjC.release(attr_str)
|
|
2552
2637
|
|
|
2553
2638
|
# Thin accent strip flush to the bottom of the bar marks
|
|
2554
2639
|
# the active tab — replaces the previous heavier full-cell
|
|
@@ -2560,6 +2645,8 @@ module Echoes
|
|
|
2560
2645
|
tab_w - 2 * accent_inset, accent_h)
|
|
2561
2646
|
end
|
|
2562
2647
|
end
|
|
2648
|
+
|
|
2649
|
+
ObjC.release(ns_para)
|
|
2563
2650
|
end
|
|
2564
2651
|
|
|
2565
2652
|
def grid_position(event_ptr)
|
|
@@ -3353,6 +3440,9 @@ module Echoes
|
|
|
3353
3440
|
@font_default_line_height = ObjC::MSG_RET_D.call(@font, ObjC.sel('defaultLineHeightForFont'))
|
|
3354
3441
|
@font_default_ascender = ascender
|
|
3355
3442
|
@font_default_family = ObjC.to_ruby_string(ObjC::MSG_PTR.call(@font, ObjC.sel('familyName')))
|
|
3443
|
+
@tab_font_line_height = @tab_font ?
|
|
3444
|
+
ObjC::MSG_RET_D.call(@tab_font, ObjC.sel('defaultLineHeightForFont')) :
|
|
3445
|
+
@font_default_line_height
|
|
3356
3446
|
@font_y_offset_cache = {}
|
|
3357
3447
|
|
|
3358
3448
|
# Propagate cell metrics to all pane screens (sixel sizing,
|
data/lib/echoes/objc.rb
CHANGED
|
@@ -53,6 +53,8 @@ module Echoes
|
|
|
53
53
|
MSG_PTR_RECT = new_msg([P, P, D, D, D, D], P) # initWithFrame:
|
|
54
54
|
MSG_VOID_RECT = new_msg([P, P, D, D, D, D], V) # NSRectFill equivalent
|
|
55
55
|
MSG_VOID_RECT_1 = new_msg([P, P, D, D, D, D, P], V) # addCursorRect:cursor:
|
|
56
|
+
# drawWithRect:options:attributes: (NSRect + NSStringDrawingOptions + NSDictionary)
|
|
57
|
+
MSG_VOID_RECT_L_1 = new_msg([P, P, D, D, D, D, L, P], V)
|
|
56
58
|
# NSGradient drawInRect:angle: (4 doubles for rect + 1 double for angle)
|
|
57
59
|
MSG_VOID_RECT_D = new_msg([P, P, D, D, D, D, D], V)
|
|
58
60
|
|
|
@@ -159,6 +161,7 @@ module Echoes
|
|
|
159
161
|
NSUnderlineStyleAttributeName = appkit_const('NSUnderlineStyleAttributeName')
|
|
160
162
|
NSStrikethroughStyleAttributeName = appkit_const('NSStrikethroughStyleAttributeName')
|
|
161
163
|
NSLigatureAttributeName = appkit_const('NSLigatureAttributeName')
|
|
164
|
+
NSParagraphStyleAttributeName = appkit_const('NSParagraphStyleAttributeName')
|
|
162
165
|
NSPasteboardTypeString = appkit_const('NSPasteboardTypeString')
|
|
163
166
|
NSPasteboardTypeFileURL = appkit_const('NSPasteboardTypeFileURL')
|
|
164
167
|
|
data/lib/echoes/version.rb
CHANGED