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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c05df4a1ad800e1b7e982aa2577a2bd6c64bfe15ec4c630aff8a561b53992072
|
|
4
|
+
data.tar.gz: dc78e9e8813c237375818a76e87b3a756cf9431e0dfb78da331cb5e8b309ca5a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8fd75f4eff8db332e59669df7359c0a0a904d9b879da99dcd33aa4dfb053a28869f80e556b9f783c6e362b8b34397ffab264d59a0aeda9c76244ad639d187dfa
|
|
7
|
+
data.tar.gz: 53a1aa9d6ee94264a31fb68c9f94ff5289415540e48461031a9b91cf52b31f8706bd9fce89dee880b2d1546c94b25605c4955545fefa0475b5aeb4c2908b8920
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
Echoes is a Ruby gem (currently a freshly scaffolded template at v0.1.0). Author: Akira Matsuda. Requires Ruby >= 3.2.0. Licensed under MIT.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
- **Install dependencies:** `bin/setup`
|
|
12
|
+
- **Run all tests:** `bundle exec rake test` (or just `bundle exec rake`, test is the default task)
|
|
13
|
+
- **Run a single test file:** `bundle exec ruby -Ilib:test test/echoes_test.rb`
|
|
14
|
+
- **Run a single test method:** `bundle exec ruby -Ilib:test test/echoes_test.rb -n test_method_name`
|
|
15
|
+
- **Interactive console:** `bin/console`
|
|
16
|
+
- **Install gem locally:** `bundle exec rake install`
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
Standard Ruby gem layout:
|
|
21
|
+
|
|
22
|
+
- `lib/echoes.rb` — Main module entry point (defines `Echoes` module)
|
|
23
|
+
- `lib/echoes/version.rb` — Version constant
|
|
24
|
+
- `sig/echoes.rbs` — RBS type signatures
|
|
25
|
+
- `test/` — Tests using **test-unit** framework (not minitest, not rspec)
|
|
26
|
+
|
|
27
|
+
## Testing
|
|
28
|
+
|
|
29
|
+
Uses the **test-unit** gem (~> 3.0). Test classes inherit from `Test::Unit::TestCase`. Test helper is at `test/test_helper.rb`.
|
|
30
|
+
|
|
31
|
+
## CI
|
|
32
|
+
|
|
33
|
+
GitHub Actions runs `bundle exec rake` on push to master and on pull requests (Ruby 4.1.0, ubuntu-latest).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleName</key>
|
|
6
|
+
<string>Echoes</string>
|
|
7
|
+
<key>CFBundleIdentifier</key>
|
|
8
|
+
<string>com.github.amatsuda.echoes</string>
|
|
9
|
+
<key>CFBundleVersion</key>
|
|
10
|
+
<string>0.2.0</string>
|
|
11
|
+
<key>CFBundleExecutable</key>
|
|
12
|
+
<string>Echoes</string>
|
|
13
|
+
<key>CFBundlePackageType</key>
|
|
14
|
+
<string>APPL</string>
|
|
15
|
+
</dict>
|
|
16
|
+
</plist>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# LaunchServices may pick the x86_64 slice of /bin/bash when launching
|
|
3
|
+
# this script-based .app bundle (script-based bundles don't get the
|
|
4
|
+
# Get Info "Open using Rosetta" toggle, but they're not necessarily
|
|
5
|
+
# launched native either). If bash runs translated, Ruby and every
|
|
6
|
+
# child rubish forks downstream — including brew — inherits the
|
|
7
|
+
# translated arch and may bail with "Cannot install under Rosetta 2".
|
|
8
|
+
# Force native arch on Apple Silicon to break that inheritance.
|
|
9
|
+
|
|
10
|
+
set -e
|
|
11
|
+
|
|
12
|
+
# Derive ECHOES_ROOT from the script's own location so the .app works
|
|
13
|
+
# inside any clone of the repo regardless of homedir or path. The
|
|
14
|
+
# script lives at <repo>/Echoes.app/Contents/MacOS/Echoes, so root
|
|
15
|
+
# is three levels up.
|
|
16
|
+
SCRIPT="${BASH_SOURCE[0]}"
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
|
18
|
+
ECHOES_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
19
|
+
ECHOES_LIB="$ECHOES_ROOT/lib"
|
|
20
|
+
ECHOES_BIN="$ECHOES_ROOT/exe/echoes"
|
|
21
|
+
|
|
22
|
+
# Pick a Ruby interpreter. Honor an explicit override via $RUBY,
|
|
23
|
+
# otherwise probe common locations (rbenv, homebrew, system) and
|
|
24
|
+
# fall back to whatever `env ruby` finds. Echoes requires Ruby
|
|
25
|
+
# >= 3.2; the first match wins, so adjust your PATH or set $RUBY
|
|
26
|
+
# if you need a specific version.
|
|
27
|
+
if [ -z "${RUBY:-}" ]; then
|
|
28
|
+
for cand in \
|
|
29
|
+
"$HOME/.rbenv/shims/ruby" \
|
|
30
|
+
/opt/homebrew/bin/ruby \
|
|
31
|
+
/usr/local/bin/ruby \
|
|
32
|
+
/usr/bin/ruby
|
|
33
|
+
do
|
|
34
|
+
if [ -x "$cand" ]; then
|
|
35
|
+
RUBY="$cand"
|
|
36
|
+
break
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
39
|
+
fi
|
|
40
|
+
RUBY="${RUBY:-$(command -v ruby || true)}"
|
|
41
|
+
if [ -z "$RUBY" ] || [ ! -x "$RUBY" ]; then
|
|
42
|
+
echo "Echoes: no Ruby interpreter found. Set \$RUBY or install one." >&2
|
|
43
|
+
exit 127
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ "$(/usr/sbin/sysctl -in hw.optional.arm64)" = "1" ]; then
|
|
47
|
+
exec /usr/bin/arch -arm64 "$RUBY" -I"$ECHOES_LIB" "$ECHOES_BIN"
|
|
48
|
+
else
|
|
49
|
+
exec "$RUBY" -I"$ECHOES_LIB" "$ECHOES_BIN"
|
|
50
|
+
fi
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleName</key>
|
|
6
|
+
<string>EchoesEmbed</string>
|
|
7
|
+
<key>CFBundleIdentifier</key>
|
|
8
|
+
<string>com.github.amatsuda.echoes-embed</string>
|
|
9
|
+
<key>CFBundleVersion</key>
|
|
10
|
+
<string>0.2.0</string>
|
|
11
|
+
<key>CFBundleExecutable</key>
|
|
12
|
+
<string>EchoesEmbed</string>
|
|
13
|
+
<key>CFBundlePackageType</key>
|
|
14
|
+
<string>APPL</string>
|
|
15
|
+
</dict>
|
|
16
|
+
</plist>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Same as Echoes.app's launcher, but exports ECHOES_EMBED=1 so panes
|
|
3
|
+
# come up running rubish in-process via the per-pane helper instead
|
|
4
|
+
# of the legacy PTY-spawn-a-shell mode. See Echoes.app's launcher for
|
|
5
|
+
# why we force native arch on Apple Silicon and how the paths /
|
|
6
|
+
# Ruby interpreter are resolved.
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
SCRIPT="${BASH_SOURCE[0]}"
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
|
12
|
+
ECHOES_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
13
|
+
ECHOES_LIB="$ECHOES_ROOT/lib"
|
|
14
|
+
ECHOES_BIN="$ECHOES_ROOT/exe/echoes"
|
|
15
|
+
|
|
16
|
+
if [ -z "${RUBY:-}" ]; then
|
|
17
|
+
for cand in \
|
|
18
|
+
"$HOME/.rbenv/shims/ruby" \
|
|
19
|
+
/opt/homebrew/bin/ruby \
|
|
20
|
+
/usr/local/bin/ruby \
|
|
21
|
+
/usr/bin/ruby
|
|
22
|
+
do
|
|
23
|
+
if [ -x "$cand" ]; then
|
|
24
|
+
RUBY="$cand"
|
|
25
|
+
break
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
fi
|
|
29
|
+
RUBY="${RUBY:-$(command -v ruby || true)}"
|
|
30
|
+
if [ -z "$RUBY" ] || [ ! -x "$RUBY" ]; then
|
|
31
|
+
echo "Echoes: no Ruby interpreter found. Set \$RUBY or install one." >&2
|
|
32
|
+
exit 127
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
export ECHOES_EMBED=1
|
|
36
|
+
|
|
37
|
+
if [ "$(/usr/sbin/sysctl -in hw.optional.arm64)" = "1" ]; then
|
|
38
|
+
exec /usr/bin/arch -arm64 "$RUBY" -I"$ECHOES_LIB" "$ECHOES_BIN"
|
|
39
|
+
else
|
|
40
|
+
exec "$RUBY" -I"$ECHOES_LIB" "$ECHOES_BIN"
|
|
41
|
+
fi
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akira Matsuda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Echoes
|
|
2
|
+
|
|
3
|
+
A pure-Ruby AppKit-based macOS terminal emulator. Echoes aims to be a
|
|
4
|
+
well-integrated host for Ruby tooling — embedding [rubish] (a Ruby
|
|
5
|
+
shell) and [rvim] (a Ruby vim) as first-class panes, with native
|
|
6
|
+
prompt rendering, structured completions, and a private OSC namespace
|
|
7
|
+
that lets in-pane Ruby tools drive UI features (gradient backgrounds,
|
|
8
|
+
proportional fonts) other terminals can't.
|
|
9
|
+
|
|
10
|
+
[rubish]: https://github.com/amatsuda/rubish
|
|
11
|
+
[rvim]: https://github.com/amatsuda/rvim
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- macOS (uses AppKit via Fiddle; no Linux/Windows support)
|
|
16
|
+
- Ruby >= 3.2
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
gem install echoes
|
|
22
|
+
echoes install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`echoes install` drops thin `Echoes.app` and `EchoesEmbed.app`
|
|
26
|
+
shortcuts into `~/Applications/` so the app shows up in Spotlight,
|
|
27
|
+
Dock, and Cmd-Space. Each shortcut is a one-line wrapper that
|
|
28
|
+
`exec`s into the gem-bundled launcher; re-run `echoes install` after
|
|
29
|
+
each `gem update echoes` to refresh the path. `echoes uninstall`
|
|
30
|
+
removes them.
|
|
31
|
+
|
|
32
|
+
To run from a clone instead:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
git clone https://github.com/amatsuda/echoes
|
|
36
|
+
cd echoes
|
|
37
|
+
bin/setup
|
|
38
|
+
open Echoes.app # GUI (PTY-spawned shell per pane)
|
|
39
|
+
open EchoesEmbed.app # GUI with rubish embedded per pane
|
|
40
|
+
bundle exec exe/echoes -t # TTY mode
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What's in the box
|
|
44
|
+
|
|
45
|
+
- **Echoes.app** — terminal mode. Spawns the user's `$SHELL` per
|
|
46
|
+
pane via PTY, like any other terminal emulator.
|
|
47
|
+
- **EchoesEmbed.app** — same window/UI, but each pane runs `rubish`
|
|
48
|
+
in-process via a per-pane helper subprocess. Line editing, prompt
|
|
49
|
+
rendering, and tab completion happen natively in Echoes (no ANSI
|
|
50
|
+
roundtrip); only command output flows over the pty.
|
|
51
|
+
- **Edit File…** (Cmd+Shift+E) — opens an rvim-backed editor pane.
|
|
52
|
+
Insert mode, `:w`, `:q`, search, visual mode, undo — the full vim
|
|
53
|
+
surface. The dialog opens at the active pane's pwd.
|
|
54
|
+
|
|
55
|
+
## Keyboard shortcuts
|
|
56
|
+
|
|
57
|
+
| Shortcut | Action |
|
|
58
|
+
|---------------------------|-----------------------------------------|
|
|
59
|
+
| Cmd+N | New window |
|
|
60
|
+
| Cmd+T | New tab |
|
|
61
|
+
| Cmd+W | Close tab |
|
|
62
|
+
| Cmd+Shift+W | Close pane |
|
|
63
|
+
| Cmd+Shift+E | Edit file… (rvim pane) |
|
|
64
|
+
| Cmd+D | Split pane right |
|
|
65
|
+
| Cmd+Shift+D | Split pane down |
|
|
66
|
+
| Cmd+] / Cmd+[ | Next / previous pane |
|
|
67
|
+
| Cmd+Shift+] / Cmd+Shift+[ | Next / previous tab |
|
|
68
|
+
| Cmd++ / Cmd+- / Cmd+0 | Bigger / smaller / reset font |
|
|
69
|
+
| Cmd+F | Find |
|
|
70
|
+
| Cmd+G / Cmd+Shift+G | Find next / previous |
|
|
71
|
+
| Cmd+Shift+P | Toggle mouse pointer visibility |
|
|
72
|
+
| Cmd+Shift+C | Toggle copy mode |
|
|
73
|
+
| Cmd+Ctrl+F | Enter / leave full screen |
|
|
74
|
+
|
|
75
|
+
## OSC extensions
|
|
76
|
+
|
|
77
|
+
Echoes recognizes a private OSC namespace under code `7772`. Other
|
|
78
|
+
terminals ignore unknown OSC codes, so emitters degrade gracefully.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
\e]7772;bg-color;#rrggbb\a
|
|
82
|
+
\e]7772;bg-gradient;type=linear:angle=N:colors=#rrggbb,#rrggbb\a
|
|
83
|
+
\e]7772;bg-fill;color=#rrggbb:rect=row1,col1,row2,col2\a
|
|
84
|
+
\e]7772;bg-clear\a
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`bg-fill` calls accumulate, so a presentation tool can build up a
|
|
88
|
+
slide layout (header bar, sidebar, accent stripe) on top of a base
|
|
89
|
+
`bg-color` or `bg-gradient`. `bg-clear` wipes both the base layer
|
|
90
|
+
and all fills.
|
|
91
|
+
|
|
92
|
+
OSC 66 is also extended:
|
|
93
|
+
|
|
94
|
+
- `f=Family Name` selects a font family for the multicell glyph.
|
|
95
|
+
Proportional fonts are measured per-glyph at layout time so the
|
|
96
|
+
cells reserved match the actual rendered width — `Hello` in Noto
|
|
97
|
+
Serif at 2× lays out cleanly without overflow or gaps.
|
|
98
|
+
- `h=` (halign) is honored for non-fractional / proportional text:
|
|
99
|
+
the whole string lands in a `s × source_chars` cell block, with
|
|
100
|
+
the renderer's existing center / right-align math applied.
|
|
101
|
+
|
|
102
|
+
A small Ruby helper (`Echoes::Client`) lets in-pane Ruby tools emit
|
|
103
|
+
these without hand-rolling escape sequences:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
require 'echoes/client'
|
|
107
|
+
|
|
108
|
+
Echoes::Client.bg_gradient(from: '#1a1a2e', to: '#16213e', angle: 90)
|
|
109
|
+
Echoes::Client.bg_fill('#ff6b35', row1: 0, col1: 0, row2: 2, col2: 79)
|
|
110
|
+
Echoes::Client.styled_text("Title", scale: 3, family: "Helvetica Neue")
|
|
111
|
+
Echoes::Client.bg_clear
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
bin/setup
|
|
118
|
+
bundle exec rake test # run all tests
|
|
119
|
+
bundle exec exe/echoes # launch from the working tree
|
|
120
|
+
bin/console # irb with the gem loaded
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`rake app` syncs `CFBundleVersion` in both bundles' `Info.plist`
|
|
124
|
+
files with `Echoes::VERSION` (run after a version bump).
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
Bug reports and pull requests welcome at
|
|
129
|
+
<https://github.com/amatsuda/echoes>.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
task default: :test
|
|
13
|
+
|
|
14
|
+
desc "Sync .app Info.plist CFBundleVersion with Echoes::VERSION"
|
|
15
|
+
task :app do
|
|
16
|
+
# The .app bundles (Echoes.app, EchoesEmbed.app) are committed
|
|
17
|
+
# to the repo, including their MacOS/ launcher scripts which
|
|
18
|
+
# locate `lib/` and `exe/echoes` from the script's own path —
|
|
19
|
+
# no per-machine rewrite needed. The one thing that drifts on a
|
|
20
|
+
# version bump is each bundle's Info.plist CFBundleVersion. This
|
|
21
|
+
# task patches that line in place; everything else (launchers,
|
|
22
|
+
# bundle id, package type) is left alone so any hand-edits the
|
|
23
|
+
# bundles have picked up survive.
|
|
24
|
+
require_relative "lib/echoes/version"
|
|
25
|
+
version = Echoes::VERSION
|
|
26
|
+
|
|
27
|
+
plists = %w[Echoes.app/Contents/Info.plist EchoesEmbed.app/Contents/Info.plist]
|
|
28
|
+
plists.each do |path|
|
|
29
|
+
unless File.exist?(path)
|
|
30
|
+
warn "skipping #{path}: not found"
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
text = File.read(path)
|
|
34
|
+
new_text = text.sub(
|
|
35
|
+
%r{(<key>CFBundleVersion</key>\s*<string>)[^<]*(</string>)},
|
|
36
|
+
"\\1#{version}\\2"
|
|
37
|
+
)
|
|
38
|
+
if text == new_text
|
|
39
|
+
puts "#{path}: already at #{version}"
|
|
40
|
+
else
|
|
41
|
+
File.write(path, new_text)
|
|
42
|
+
puts "#{path}: CFBundleVersion → #{version}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/exe/echoes
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
case ARGV.first
|
|
5
|
+
when 'install', 'uninstall'
|
|
6
|
+
require 'echoes/installer'
|
|
7
|
+
Echoes::Installer.public_send(ARGV.shift)
|
|
8
|
+
else
|
|
9
|
+
require 'echoes'
|
|
10
|
+
if ARGV.delete('--tty') || ARGV.delete('-t')
|
|
11
|
+
Echoes::Terminal.new.run
|
|
12
|
+
else
|
|
13
|
+
Echoes::GUI.new.run
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/echoes/cell.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Echoes
|
|
4
|
+
class Cell
|
|
5
|
+
attr_accessor :char, :fg, :bg, :bold, :italic, :underline, :underline_color, :inverse, :faint, :strikethrough, :blink, :concealed, :width, :multicell, :hyperlink
|
|
6
|
+
|
|
7
|
+
def initialize(char = " ", fg: nil, bg: nil, bold: false, underline: false, inverse: false, width: 1)
|
|
8
|
+
@char = char
|
|
9
|
+
@fg = fg
|
|
10
|
+
@bg = bg
|
|
11
|
+
@bold = bold
|
|
12
|
+
@italic = false
|
|
13
|
+
@underline = underline
|
|
14
|
+
@inverse = inverse
|
|
15
|
+
@faint = false
|
|
16
|
+
@strikethrough = false
|
|
17
|
+
@width = width
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset!
|
|
21
|
+
@char = " "
|
|
22
|
+
@fg = nil
|
|
23
|
+
@bg = nil
|
|
24
|
+
@bold = false
|
|
25
|
+
@italic = false
|
|
26
|
+
@underline = false
|
|
27
|
+
@inverse = false
|
|
28
|
+
@faint = false
|
|
29
|
+
@strikethrough = false
|
|
30
|
+
@blink = false
|
|
31
|
+
@concealed = false
|
|
32
|
+
@width = 1
|
|
33
|
+
@multicell = nil
|
|
34
|
+
@hyperlink = nil
|
|
35
|
+
@underline_color = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def copy_from(other)
|
|
39
|
+
@char = other.char
|
|
40
|
+
@fg = other.fg
|
|
41
|
+
@bg = other.bg
|
|
42
|
+
@bold = other.bold
|
|
43
|
+
@italic = other.italic
|
|
44
|
+
@underline = other.underline
|
|
45
|
+
@underline_color = other.underline_color
|
|
46
|
+
@inverse = other.inverse
|
|
47
|
+
@faint = other.faint
|
|
48
|
+
@strikethrough = other.strikethrough
|
|
49
|
+
@blink = other.blink
|
|
50
|
+
@concealed = other.concealed
|
|
51
|
+
@hyperlink = other.hyperlink
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Echoes
|
|
4
|
+
# Helpers that emit Echoes-private OSC sequences for tools running
|
|
5
|
+
# inside an Echoes pane (e.g. a Ruby presentation tool that wants
|
|
6
|
+
# Keynote-style gradient slide backgrounds). Other terminals ignore
|
|
7
|
+
# the OSC code, so emitters degrade gracefully.
|
|
8
|
+
#
|
|
9
|
+
# Example:
|
|
10
|
+
# Echoes::Client.bg_gradient(from: '#1a1a2e', to: '#16213e', angle: 90)
|
|
11
|
+
# # ...later, restore the solid background:
|
|
12
|
+
# Echoes::Client.bg_clear
|
|
13
|
+
module Client
|
|
14
|
+
OSC = "\e]7772"
|
|
15
|
+
BEL = "\a"
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Paint a linear gradient as the pane's background. `angle` is in
|
|
20
|
+
# degrees (0 = left→right, 90 = bottom→top, matches NSGradient).
|
|
21
|
+
# Pass an array of hex strings via `colors:` for endpoints beyond
|
|
22
|
+
# two (the renderer currently uses first/last only).
|
|
23
|
+
def bg_gradient(from: nil, to: nil, colors: nil, angle: 0, type: :linear, io: $stdout)
|
|
24
|
+
colors ||= [from, to]
|
|
25
|
+
colors = colors.compact.map(&:to_s)
|
|
26
|
+
raise ArgumentError, 'need at least 2 colors' if colors.size < 2
|
|
27
|
+
|
|
28
|
+
args = "type=#{type}:angle=#{angle}:colors=#{colors.join(',')}"
|
|
29
|
+
io.write("#{OSC};bg-gradient;#{args}#{BEL}")
|
|
30
|
+
io.flush if io.respond_to?(:flush)
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Paint the pane's background with a single solid color. The
|
|
35
|
+
# color paints beneath cell content, so cells with their own
|
|
36
|
+
# bg color (selection, themed cells, etc.) still occlude
|
|
37
|
+
# correctly. Pair with bg_clear to revert.
|
|
38
|
+
#
|
|
39
|
+
# Example:
|
|
40
|
+
# Echoes::Client.bg_color('#1a1a2e')
|
|
41
|
+
def bg_color(color, io: $stdout)
|
|
42
|
+
io.write("#{OSC};bg-color;#{color}#{BEL}")
|
|
43
|
+
io.flush if io.respond_to?(:flush)
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Paint a rectangular region of the pane on top of the base
|
|
48
|
+
# background. Calls accumulate — emit several to build up a
|
|
49
|
+
# layout (header/footer bars, sidebars, accent stripes, etc.).
|
|
50
|
+
# `bg_clear` wipes the whole list along with the base background.
|
|
51
|
+
#
|
|
52
|
+
# Coordinates are 0-indexed cell positions, inclusive on both
|
|
53
|
+
# ends — i.e. row1=0, col1=0, row2=2, col2=9 paints a 3x10 block.
|
|
54
|
+
#
|
|
55
|
+
# Example:
|
|
56
|
+
# Echoes::Client.bg_fill('#222', row1: 0, col1: 0, row2: 0, col2: 79) # status bar
|
|
57
|
+
# Echoes::Client.bg_fill('#3a3', row1: 24, col1: 0, row2: 24, col2: 79) # footer
|
|
58
|
+
def bg_fill(color, row1:, col1:, row2:, col2:, io: $stdout)
|
|
59
|
+
args = "color=#{color}:rect=#{row1},#{col1},#{row2},#{col2}"
|
|
60
|
+
io.write("#{OSC};bg-fill;#{args}#{BEL}")
|
|
61
|
+
io.flush if io.respond_to?(:flush)
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Drop any pane background override (bg-color/bg-gradient) AND
|
|
66
|
+
# all bg-fill overlays. Safe to call when nothing is set.
|
|
67
|
+
def bg_clear(io: $stdout)
|
|
68
|
+
io.write("#{OSC};bg-clear#{BEL}")
|
|
69
|
+
io.flush if io.respond_to?(:flush)
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
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.
|
|
78
|
+
#
|
|
79
|
+
# Examples:
|
|
80
|
+
# Echoes::Client.styled_text("Title", scale: 3, family: "Helvetica Neue")
|
|
81
|
+
# Echoes::Client.styled_text("• item", scale: 1, family: "Menlo")
|
|
82
|
+
def styled_text(text, scale: 1, width: nil, frac_n: nil, frac_d: nil,
|
|
83
|
+
valign: nil, halign: nil, family: nil, io: $stdout)
|
|
84
|
+
meta = +"s=#{scale}"
|
|
85
|
+
meta << ":w=#{width}" if width
|
|
86
|
+
meta << ":n=#{frac_n}" if frac_n
|
|
87
|
+
meta << ":d=#{frac_d}" if frac_d
|
|
88
|
+
meta << ":v=#{valign}" if valign
|
|
89
|
+
meta << ":h=#{halign}" if halign
|
|
90
|
+
meta << ":f=#{family}" if family
|
|
91
|
+
io.write("\e]66;#{meta};#{text}\a")
|
|
92
|
+
io.flush if io.respond_to?(:flush)
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Echoes
|
|
4
|
+
class Configuration
|
|
5
|
+
def initialize
|
|
6
|
+
@font_size = 14.0
|
|
7
|
+
@rows = 24
|
|
8
|
+
@cols = 80
|
|
9
|
+
@shell = ENV['SHELL'] || '/bin/bash'
|
|
10
|
+
@scrollback_limit = 1000
|
|
11
|
+
@foreground = [0.9, 0.9, 0.9]
|
|
12
|
+
@background = [0.0, 0.0, 0.0]
|
|
13
|
+
@cursor_color = [0.7, 0.7, 0.7, 0.5]
|
|
14
|
+
@font_family = nil
|
|
15
|
+
@window_title = 'Echoes'
|
|
16
|
+
@tab_position = :top
|
|
17
|
+
@color_palette = nil
|
|
18
|
+
@term = 'xterm-256color'
|
|
19
|
+
@word_separators = ' @*.:/\\()"\'-:,.;<>~!#$%^&*|+=[]{}~?│'
|
|
20
|
+
@selection_color = [0.2, 0.4, 0.7]
|
|
21
|
+
@pane_divider_color = [0.4, 0.4, 0.4]
|
|
22
|
+
@active_pane_border_color = [0.3, 0.5, 0.8]
|
|
23
|
+
@copy_mode_cursor_color = [0.8, 0.7, 0.2]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def font_family(val = nil)
|
|
27
|
+
val ? @font_family = val : @font_family
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def font_size(val = nil)
|
|
31
|
+
val ? @font_size = val.to_f : @font_size
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def rows(val = nil)
|
|
35
|
+
val ? @rows = val.to_i : @rows
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cols(val = nil)
|
|
39
|
+
val ? @cols = val.to_i : @cols
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def shell(val = nil)
|
|
43
|
+
val ? @shell = val : @shell
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def scrollback_limit(val = nil)
|
|
47
|
+
val ? @scrollback_limit = val.to_i : @scrollback_limit
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def foreground(*args)
|
|
51
|
+
args.empty? ? @foreground : @foreground = parse_color(args)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def background(*args)
|
|
55
|
+
args.empty? ? @background : @background = parse_color(args)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cursor_color(*args)
|
|
59
|
+
args.empty? ? @cursor_color : @cursor_color = parse_color(args)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def window_title(val = nil)
|
|
63
|
+
val ? @window_title = val : @window_title
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def tab_position(val = nil)
|
|
67
|
+
val ? @tab_position = val.to_sym : @tab_position
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def term(val = nil)
|
|
71
|
+
val ? @term = val : @term
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def word_separators(val = nil)
|
|
75
|
+
val ? @word_separators = val : @word_separators
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def selection_color(*args)
|
|
79
|
+
args.empty? ? @selection_color : @selection_color = parse_color(args)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def pane_divider_color(*args)
|
|
83
|
+
args.empty? ? @pane_divider_color : @pane_divider_color = parse_color(args)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def active_pane_border_color(*args)
|
|
87
|
+
args.empty? ? @active_pane_border_color : @active_pane_border_color = parse_color(args)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def copy_mode_cursor_color(*args)
|
|
91
|
+
args.empty? ? @copy_mode_cursor_color : @copy_mode_cursor_color = parse_color(args)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def color_palette(val = nil)
|
|
95
|
+
if val
|
|
96
|
+
@color_palette = val.map { |c| c.is_a?(String) ? parse_color([c]) : c.map(&:to_f) }
|
|
97
|
+
else
|
|
98
|
+
@color_palette
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def parse_color(args)
|
|
105
|
+
if args.size == 1 && args[0].is_a?(String)
|
|
106
|
+
hex = args[0].delete_prefix('#')
|
|
107
|
+
r = hex[0, 2].to_i(16) / 255.0
|
|
108
|
+
g = hex[2, 2].to_i(16) / 255.0
|
|
109
|
+
b = hex[4, 2].to_i(16) / 255.0
|
|
110
|
+
if hex.size == 8
|
|
111
|
+
a = hex[6, 2].to_i(16) / 255.0
|
|
112
|
+
[r, g, b, a]
|
|
113
|
+
else
|
|
114
|
+
[r, g, b]
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
args.map(&:to_f)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
CONFIG_PATH = File.join(Dir.home, '.config', 'echoes', 'echoes.conf')
|
|
123
|
+
|
|
124
|
+
def self.config
|
|
125
|
+
@config ||= Configuration.new
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.load_config
|
|
129
|
+
if File.exist?(CONFIG_PATH)
|
|
130
|
+
config.instance_eval(File.read(CONFIG_PATH), CONFIG_PATH)
|
|
131
|
+
end
|
|
132
|
+
rescue SyntaxError, StandardError => e
|
|
133
|
+
warn "echoes: error loading #{CONFIG_PATH}: #{e.message}"
|
|
134
|
+
end
|
|
135
|
+
end
|