reflex-terminal 0.1.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/.doc/ext/reflex-terminal/native.cpp +19 -0
- data/.doc/ext/reflex-terminal/renderer.cpp +110 -0
- data/.doc/ext/reflex-terminal/terminal.cpp +530 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
- data/.github/workflows/release-gem.yml +60 -0
- data/.github/workflows/tag.yml +35 -0
- data/.github/workflows/test.yml +46 -0
- data/.github/workflows/utils.rb +127 -0
- data/CONTRIBUTING.md +7 -0
- data/ChangeLog.md +6 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/examples/terminal.rb +101 -0
- data/ext/reflex-terminal/defs.h +17 -0
- data/ext/reflex-terminal/extconf.rb +29 -0
- data/ext/reflex-terminal/native.cpp +19 -0
- data/ext/reflex-terminal/renderer.cpp +118 -0
- data/ext/reflex-terminal/terminal.cpp +572 -0
- data/include/reflex/terminal.h +233 -0
- data/include/reflex-terminal/defs.h +36 -0
- data/include/reflex-terminal/renderer.h +57 -0
- data/include/reflex-terminal/ruby/renderer.h +41 -0
- data/include/reflex-terminal/ruby/terminal.h +43 -0
- data/include/reflex-terminal/ruby.h +11 -0
- data/include/reflex-terminal.h +13 -0
- data/lib/reflex/bell_event.rb +15 -0
- data/lib/reflex/terminal.rb +168 -0
- data/lib/reflex/terminal_view.rb +268 -0
- data/lib/reflex-terminal/ext.rb +2 -0
- data/lib/reflex-terminal/extension.rb +41 -0
- data/lib/reflex-terminal.rb +7 -0
- data/reflex-terminal.gemspec +39 -0
- data/src/pty.cpp +236 -0
- data/src/renderer.cpp +365 -0
- data/src/terminal.cpp +1547 -0
- data/src/terminal.h +66 -0
- data/src/win32/pty.cpp +387 -0
- data/test/helper.rb +11 -0
- data/test/test_renderer.rb +113 -0
- data/test/test_terminal.rb +683 -0
- data/test/test_terminal_view.rb +182 -0
- metadata +152 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Release Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v[0-9]*']
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
runs-on: macos-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: ruby 3.2
|
|
16
|
+
uses: ruby/setup-ruby@v1
|
|
17
|
+
with:
|
|
18
|
+
ruby-version: 3.2
|
|
19
|
+
|
|
20
|
+
- name: checkout
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: setup gems
|
|
24
|
+
run: bundle install
|
|
25
|
+
|
|
26
|
+
- name: setup dependencies
|
|
27
|
+
run: "ruby -I.github/workflows -rutils -e 'setup_dependencies'"
|
|
28
|
+
|
|
29
|
+
- name: packages
|
|
30
|
+
run: bundle exec rake packages
|
|
31
|
+
|
|
32
|
+
- name: setup zig
|
|
33
|
+
if: github.event.repository.name == 'reflex-terminal'
|
|
34
|
+
uses: mlugg/setup-zig@v2
|
|
35
|
+
with:
|
|
36
|
+
version: 0.16.0
|
|
37
|
+
|
|
38
|
+
- name: test
|
|
39
|
+
run: bundle exec rake test
|
|
40
|
+
|
|
41
|
+
- name: create gem
|
|
42
|
+
id: gem
|
|
43
|
+
run: |
|
|
44
|
+
bundle exec rake gem
|
|
45
|
+
echo path=$(ruby -e 'print Dir.glob("*.gem").first') >> $GITHUB_OUTPUT
|
|
46
|
+
|
|
47
|
+
- name: create github release
|
|
48
|
+
env:
|
|
49
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
50
|
+
run: ruby -I.github/workflows -rutils -e 'release(*ARGV)' ./${{ steps.gem.outputs.path }}
|
|
51
|
+
|
|
52
|
+
- name: upload to rubygems
|
|
53
|
+
env:
|
|
54
|
+
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_AUTH_TOKEN }}
|
|
55
|
+
run: |
|
|
56
|
+
mkdir -p $HOME/.gem
|
|
57
|
+
touch $HOME/.gem/credentials
|
|
58
|
+
chmod 0600 $HOME/.gem/credentials
|
|
59
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
60
|
+
bundle exec rake upload
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Tag
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
tag:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: ruby 3.2
|
|
13
|
+
uses: ruby/setup-ruby@v1
|
|
14
|
+
with:
|
|
15
|
+
ruby-version: 3.2
|
|
16
|
+
|
|
17
|
+
- name: checkout
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
token: ${{ secrets.PAT }}
|
|
22
|
+
|
|
23
|
+
- name: setup dependencies
|
|
24
|
+
run: "ruby -I.github/workflows -rutils -e 'setup_dependencies only: :xot'"
|
|
25
|
+
|
|
26
|
+
- name: setup user name and email
|
|
27
|
+
run: |
|
|
28
|
+
git config --global user.email "xordog@gmail.com"
|
|
29
|
+
git config --global user.name "xord"
|
|
30
|
+
|
|
31
|
+
- name: tag versions
|
|
32
|
+
run: "ruby -I.github/workflows -rutils -e 'tag_versions'"
|
|
33
|
+
|
|
34
|
+
- name: push tags
|
|
35
|
+
run: git push origin --tags
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: macos-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: ruby 3.2
|
|
14
|
+
uses: ruby/setup-ruby@v1
|
|
15
|
+
with:
|
|
16
|
+
ruby-version: 3.2
|
|
17
|
+
|
|
18
|
+
- name: checkout
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: setup gems
|
|
22
|
+
run: bundle install
|
|
23
|
+
|
|
24
|
+
- name: setup dependencies
|
|
25
|
+
run: "ruby -I.github/workflows -rutils -e 'setup_dependencies'"
|
|
26
|
+
|
|
27
|
+
- name: packages
|
|
28
|
+
run: bundle exec rake verbose packages
|
|
29
|
+
|
|
30
|
+
- name: setup zig
|
|
31
|
+
if: github.event.repository.name == 'reflex-terminal'
|
|
32
|
+
uses: mlugg/setup-zig@v2
|
|
33
|
+
with:
|
|
34
|
+
version: 0.16.0
|
|
35
|
+
|
|
36
|
+
- name: vendor
|
|
37
|
+
run: bundle exec rake vendor
|
|
38
|
+
|
|
39
|
+
- name: lib
|
|
40
|
+
run: bundle exec rake verbose lib
|
|
41
|
+
|
|
42
|
+
- name: ext
|
|
43
|
+
run: bundle exec rake verbose ext
|
|
44
|
+
|
|
45
|
+
- name: test
|
|
46
|
+
run: bundle exec rake verbose test
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require 'shellwords'
|
|
2
|
+
|
|
3
|
+
ALL_REPO = 'xord/all'
|
|
4
|
+
ALL_DIR = '../all'
|
|
5
|
+
ALL_FETCH_DEPTH = 100
|
|
6
|
+
|
|
7
|
+
RENAMES = {reflex: 'reflexion'}
|
|
8
|
+
|
|
9
|
+
def sh(cmd)
|
|
10
|
+
puts cmd
|
|
11
|
+
system cmd
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def setup_dependencies(only: nil)
|
|
15
|
+
gemspec_path = `git ls-files`.lines(chomp: true).find {|l| l =~ /\.gemspec$/}
|
|
16
|
+
return unless gemspec_path
|
|
17
|
+
|
|
18
|
+
gemspec = File.read gemspec_path
|
|
19
|
+
name = File.basename gemspec_path, '.gemspec'
|
|
20
|
+
|
|
21
|
+
exts = File.readlines('Rakefile')
|
|
22
|
+
.map {|l| l[%r|^\s*require\W+([\w\-\_]+)/extension\W+$|, 1]}
|
|
23
|
+
.compact
|
|
24
|
+
.reject {|ext| ext == name}
|
|
25
|
+
exts = exts & [only].flatten.map(&:to_s) if only
|
|
26
|
+
return if exts.empty?
|
|
27
|
+
|
|
28
|
+
unless setup_dependencies_via_monorepo(exts)
|
|
29
|
+
setup_dependencies_via_each_repo_by_version(gemspec, exts)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
exts.each {|ext| sh %( cd ../#{ext} && rake ext )}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def setup_dependencies_via_monorepo(exts)
|
|
36
|
+
return false unless checkout_monorepo
|
|
37
|
+
exts.each {|ext| sh %( ln -snf all/#{ext} ../#{ext} )}
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def checkout_monorepo()
|
|
42
|
+
uuid = `git log -1 --format=%B`[/^\[\[([0-9a-fA-F-]+)\]\]$/, 1]
|
|
43
|
+
return false unless uuid
|
|
44
|
+
|
|
45
|
+
commit = setup_monorepo uuid
|
|
46
|
+
return false unless commit
|
|
47
|
+
|
|
48
|
+
Dir.chdir(ALL_DIR) {sh %( git checkout -q #{commit} )}
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def setup_monorepo(uuid)
|
|
53
|
+
unless File.directory? ALL_DIR
|
|
54
|
+
url = "https://github.com/#{ALL_REPO}.git"
|
|
55
|
+
sh %( git clone --no-tags --depth #{ALL_FETCH_DEPTH} #{url} #{ALL_DIR} )
|
|
56
|
+
end
|
|
57
|
+
loop do
|
|
58
|
+
commit = find_monorepo_commit uuid
|
|
59
|
+
return commit if commit
|
|
60
|
+
|
|
61
|
+
deepened = Dir.chdir ALL_DIR do
|
|
62
|
+
before = `git rev-list --count HEAD`.to_i
|
|
63
|
+
sh %( git fetch --deepen #{ALL_FETCH_DEPTH} )
|
|
64
|
+
`git rev-list --count HEAD`.to_i > before
|
|
65
|
+
end
|
|
66
|
+
return nil unless deepened
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find_monorepo_commit(uuid)
|
|
71
|
+
Dir.chdir ALL_DIR do
|
|
72
|
+
out = `git log origin/HEAD -F --grep="[[#{uuid}]]" --format=%H -1`.strip
|
|
73
|
+
out.empty? ? nil : out
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def setup_dependencies_via_each_repo_by_version(gemspec, exts)
|
|
78
|
+
exts.each do |ext|
|
|
79
|
+
gem = RENAMES[ext.to_sym].then {|s| s || ext}
|
|
80
|
+
ver = gemspec[/add_dependency.*['"]#{gem}['"].*['"]\s*~>\s*([\d\.]+)\s*['"]/, 1]
|
|
81
|
+
opts = '-c advice.detachedHead=false --depth 1'
|
|
82
|
+
clone = "git clone #{opts} https://github.com/xord/#{ext}.git ../#{ext}"
|
|
83
|
+
|
|
84
|
+
# 'rake subtree:push' pushes all subrepos, so cloning by new tag
|
|
85
|
+
# often fails before tagging each new tag
|
|
86
|
+
sh %( #{clone} --branch v#{ver} || #{clone} )
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def tag_versions()
|
|
91
|
+
changes = changelogs
|
|
92
|
+
tags = `git tag`.lines chomp: true
|
|
93
|
+
vers = `git log --oneline ./VERSION`
|
|
94
|
+
.lines(chomp: true)
|
|
95
|
+
.map {|line| line.split.first[/^\w+$/]}
|
|
96
|
+
.map {|sha| [`git cat-file -p #{sha}:./VERSION 2>/dev/null`[/[\d\.]+/], sha]}
|
|
97
|
+
.select {|ver, sha| ver && sha}
|
|
98
|
+
.reverse
|
|
99
|
+
.to_h
|
|
100
|
+
|
|
101
|
+
vers.to_a.reverse.each do |ver, sha|
|
|
102
|
+
tag = "v#{ver}"
|
|
103
|
+
break if tags.include?(tag)
|
|
104
|
+
sh %( git tag -a -m \"#{changes[tag]&.gsub '"', '\\"'}\" #{tag} #{sha} )
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def release(*paths)
|
|
109
|
+
tag = ENV['GITHUB_REF']&.sub(%r|^refs/tags/|, '') || raise('GITHUB_REF tag not set')
|
|
110
|
+
notes = (changelogs[tag] || '').shellescape
|
|
111
|
+
paths = paths.flatten.join ' '
|
|
112
|
+
|
|
113
|
+
sh(%( gh release create #{tag} #{paths} --notes #{notes} )) ||
|
|
114
|
+
sh(%( gh release upload #{tag} #{paths} --clobber )) ||
|
|
115
|
+
raise('failed to upload to releases')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def changelogs()
|
|
119
|
+
File.read('ChangeLog.md')
|
|
120
|
+
.split(/^\s*##\s*\[\s*(v[\d\.]+)\s*\].*$/)
|
|
121
|
+
.slice(1..)
|
|
122
|
+
.each_slice(2)
|
|
123
|
+
.to_h
|
|
124
|
+
.transform_values(&:strip!)
|
|
125
|
+
rescue Errno::ENOENT
|
|
126
|
+
raise 'failed to get changelogs'
|
|
127
|
+
end
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Contribution Guide
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing!
|
|
4
|
+
However, this repository does not accept pull requests.
|
|
5
|
+
Instead, please submit your changes to the [xord/all](https://github.com/xord/all) monorepo, which serves as the primary repository for all our main libraries.
|
|
6
|
+
|
|
7
|
+
For any questions, feel free to open an issue.
|
data/ChangeLog.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xord.org
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Reflex Terminal - A terminal emulator for Reflex
|
|
2
|
+
|
|
3
|
+
**Status: Usable, but the API is not stable yet.**
|
|
4
|
+
|
|
5
|
+
reflex-terminal provides a terminal emulator for the
|
|
6
|
+
[Reflex](https://github.com/xord/reflex) GUI toolkit:
|
|
7
|
+
|
|
8
|
+
- `Reflex::Terminal` — a headless terminal emulator model. It spawns a shell
|
|
9
|
+
on a PTY and maintains the screen state (cells, colors, scrollback, reflow
|
|
10
|
+
on resize) using [libghostty-vt](https://libghostty.tip.ghostty.org/),
|
|
11
|
+
the terminal emulation core extracted from
|
|
12
|
+
[Ghostty](https://github.com/ghostty-org/ghostty).
|
|
13
|
+
- `Reflex::TerminalView` — a `Reflex::View` that renders a `Terminal` and
|
|
14
|
+
feeds keyboard / mouse input into it.
|
|
15
|
+
|
|
16
|
+
Full-screen programs work: emacs and mouse-driven terminal multiplexers run
|
|
17
|
+
in it, with colors, CJK text, reflow on resize and scrollback.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require 'reflex-terminal'
|
|
23
|
+
|
|
24
|
+
win = Reflex::Window.new {
|
|
25
|
+
title 'Terminal'
|
|
26
|
+
frame 100, 100, 720, 450
|
|
27
|
+
}
|
|
28
|
+
win.add Reflex::TerminalView.new
|
|
29
|
+
win.show
|
|
30
|
+
|
|
31
|
+
Reflex.start
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
See `examples/terminal.rb`.
|
|
35
|
+
|
|
36
|
+
### Without a child process
|
|
37
|
+
|
|
38
|
+
A `Terminal` does not have to run a shell. Feed it bytes and it renders
|
|
39
|
+
whatever speaks terminal escape sequences — a build log, a recorded
|
|
40
|
+
session — with its colors and cursor motion applied:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
t = Reflex::Terminal.new 120, 40
|
|
44
|
+
t.feed File.binread('build.log')
|
|
45
|
+
t.update
|
|
46
|
+
puts t.lines
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Driving an interactive program over your own transport takes one more call:
|
|
50
|
+
`#read_pending_input` hands back the bytes the terminal wants to send
|
|
51
|
+
upstream, which are the query responses it generates on its own plus any
|
|
52
|
+
encoded key and mouse events.
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
- macOS, Linux or Windows
|
|
57
|
+
- Ruby 3.0+
|
|
58
|
+
- [Zig](https://ziglang.org/) **0.16.0** — required to build libghostty-vt
|
|
59
|
+
from source into `vendor/ghostty/`. On macOS `rake packages` installs it
|
|
60
|
+
through Homebrew; elsewhere take the 0.16.0 build for your platform from
|
|
61
|
+
the [downloads page](https://ziglang.org/download/) and put it on `PATH`,
|
|
62
|
+
since packaged versions rarely match the one the pinned ghostty commit
|
|
63
|
+
asks for.
|
|
64
|
+
|
|
65
|
+
## Build
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
$ rake packages # macOS: installs zig through Homebrew
|
|
69
|
+
$ rake ext # clones + builds libghostty-vt (first time only), then the extension
|
|
70
|
+
$ rake test
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Limitations
|
|
74
|
+
|
|
75
|
+
- No input method support, so Japanese and other composed text cannot be
|
|
76
|
+
typed. Reflex hands key events straight to the view without going through
|
|
77
|
+
the platform's input context, which is where composition would start.
|
|
78
|
+
- On Linux, `TerminalView` needs a font handed to it (`font:` with a
|
|
79
|
+
`Rays::Font` or a path). Rays cannot look one up by name there, so the
|
|
80
|
+
default font name does not resolve.
|
|
81
|
+
- On Windows, the single-string form of `Terminal#spawn` cannot contain
|
|
82
|
+
quotes. It runs through `cmd.exe /c`, which does not understand the
|
|
83
|
+
backslash escaping that the command line is built with. Pass the command
|
|
84
|
+
as separate arguments instead.
|
|
85
|
+
- No image protocols (Kitty graphics, Sixel).
|
|
86
|
+
- No selection or copy UI. `#lines` and `#each_history_line` return the text.
|
|
87
|
+
|
|
88
|
+
## Updating the vendored libghostty-vt
|
|
89
|
+
|
|
90
|
+
libghostty-vt is under active development and has no standalone releases yet
|
|
91
|
+
(ghostty release tags up to v1.3.1 do not contain the terminal C APIs), so
|
|
92
|
+
this gem pins a verified commit of ghostty's main branch: see the `commit:`
|
|
93
|
+
given to `use_external_library` in `Rakefile`. To update:
|
|
94
|
+
|
|
95
|
+
1. Bump that commit (check `minimum_zig_version` in ghostty's
|
|
96
|
+
`build.zig.zon` still matches the installed Zig)
|
|
97
|
+
2. `rake vendor:ghostty:update`
|
|
98
|
+
3. `rake ext test`
|
|
99
|
+
|
|
100
|
+
Once libghostty-vt gets standalone releases, switch the pin to a release tag.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- mode: ruby -*-
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
%w[../xot ../rucy ../rays ../reflex .]
|
|
5
|
+
.map {|s| File.expand_path "#{s}/lib", __dir__}
|
|
6
|
+
.each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)}
|
|
7
|
+
|
|
8
|
+
require 'rucy/rake'
|
|
9
|
+
|
|
10
|
+
require 'xot/extension'
|
|
11
|
+
require 'rucy/extension'
|
|
12
|
+
require 'rays/extension'
|
|
13
|
+
require 'reflex/extension'
|
|
14
|
+
require 'reflex-terminal/extension'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EXTENSIONS = [Xot, Rucy, Rays, Reflex, ReflexTerminal]
|
|
18
|
+
TESTS_ALONE = []
|
|
19
|
+
|
|
20
|
+
install_packages osx: %w[zig]
|
|
21
|
+
|
|
22
|
+
use_external_library 'https://github.com/ghostty-org/ghostty',
|
|
23
|
+
# libghostty-vt is not released as a standalone library yet, so pin a
|
|
24
|
+
# verified commit on ghostty's main branch. (Release tags up to v1.3.1 do
|
|
25
|
+
# not contain the terminal/render C APIs.) Bump intentionally, then run
|
|
26
|
+
# 'rake vendor:ghostty:update' and re-run the tests.
|
|
27
|
+
commit: '2de5e7d38e1354759211722a8687c0815d2cf02c',# requires Zig 0.16.0
|
|
28
|
+
incdirs: 'zig-out/include',
|
|
29
|
+
srcdirs: 'NOSRC',
|
|
30
|
+
defs: 'GHOSTTY_STATIC',
|
|
31
|
+
&proc {
|
|
32
|
+
opts = '-Demit-lib-vt -Doptimize=ReleaseFast'
|
|
33
|
+
opts += ' -Dtarget=x86_64-windows-gnu' if win32?
|
|
34
|
+
sh %( zig build #{opts} )
|
|
35
|
+
sh %( mv zig-out/lib/libghostty-vt.a zig-out/lib/libghostty-vt-static.a ) unless win32?
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
default_tasks :ext
|
|
39
|
+
use_bundler
|
|
40
|
+
build_native_library
|
|
41
|
+
build_ruby_extension
|
|
42
|
+
test_ruby_extension unless github_actions? && win32?
|
|
43
|
+
generate_documents
|
|
44
|
+
build_ruby_gem
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
%w[xot rucy rays reflex reflex-terminal]
|
|
2
|
+
.map {|s| File.expand_path "../../#{s}/lib", __dir__}
|
|
3
|
+
.each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)}
|
|
4
|
+
|
|
5
|
+
require 'reflex-terminal'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def copy(terminal)
|
|
9
|
+
text = terminal.selected_text
|
|
10
|
+
Reflex::Clipboard.text = text unless text.empty?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def copyable?(terminal)
|
|
14
|
+
terminal.selection?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def paste(terminal)
|
|
18
|
+
text = Reflex::Clipboard.text
|
|
19
|
+
return if text.to_s.empty?
|
|
20
|
+
terminal.scroll_to 0 if terminal.scroll != 0
|
|
21
|
+
terminal.paste text
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pasteable?()
|
|
25
|
+
!Reflex::Clipboard.text.to_s.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
terminal = Reflex::Terminal.new.spawn
|
|
30
|
+
view = Reflex::TerminalView.new terminal: terminal, font_size: 24
|
|
31
|
+
shortcut = Xot.osx? ? %i[command] : %i[control shift]
|
|
32
|
+
taken = []
|
|
33
|
+
flash = 0
|
|
34
|
+
flash_timer = nil
|
|
35
|
+
|
|
36
|
+
view.after :on_draw do |e|
|
|
37
|
+
next if flash <= 0
|
|
38
|
+
e.painter.fill 1, flash
|
|
39
|
+
e.painter.rect e.bounds
|
|
40
|
+
flash *= 0.9
|
|
41
|
+
flash = 0 if flash < 0.01
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
view.before :on_key_down do |e|
|
|
45
|
+
# locks are states the keyboard is left in rather than keys held for
|
|
46
|
+
# the stroke, so a shortcut has to match with them left out
|
|
47
|
+
next unless e.modifiers(locks: false).sort == shortcut.sort
|
|
48
|
+
|
|
49
|
+
case e.code
|
|
50
|
+
when Reflex::KEY_C then copy terminal
|
|
51
|
+
when Reflex::KEY_V then paste terminal
|
|
52
|
+
else next
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# the press is taken, so its release has to be taken with it or the
|
|
56
|
+
# child is told a key went up that it never saw go down. The modifier
|
|
57
|
+
# can be let go first, so the key itself is what has to be remembered
|
|
58
|
+
taken |= [e.code]
|
|
59
|
+
:skip
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
view.before :on_key_up do |e|
|
|
63
|
+
:skip if taken.delete e.code
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# on the release, not the press: a native menu runs an event loop of its
|
|
67
|
+
# own and swallows the mouse up, which would leave the window believing the
|
|
68
|
+
# button was still down and every later drag starting where it was clicked
|
|
69
|
+
view.before :on_pointer_up do |e|
|
|
70
|
+
next unless e.right?
|
|
71
|
+
m = Reflex::Menu.new
|
|
72
|
+
|
|
73
|
+
c = m.add Reflex::Menu.new('Copy')
|
|
74
|
+
c.on(:show) {|e| c.enable copyable?(terminal)}
|
|
75
|
+
c.on(:click) {|e| copy terminal}
|
|
76
|
+
|
|
77
|
+
p = m.add Reflex::Menu.new('Paste')
|
|
78
|
+
p.on(:show) {|e| p.enable pasteable?}
|
|
79
|
+
p.on(:click) {|e| paste terminal}
|
|
80
|
+
|
|
81
|
+
m.popup view, e.x, e.y
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
view.on :bell do |e|
|
|
85
|
+
flash = 0.8
|
|
86
|
+
|
|
87
|
+
flash_timer&.stop
|
|
88
|
+
flash_timer = view.interval do
|
|
89
|
+
view.redraw
|
|
90
|
+
flash_timer.stop if flash <= 0
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
win = Reflex::Window.new do
|
|
95
|
+
title 'Terminal Example'
|
|
96
|
+
frame 100, 100, 720, 450
|
|
97
|
+
end
|
|
98
|
+
win.add view
|
|
99
|
+
win.show
|
|
100
|
+
|
|
101
|
+
Reflex.start
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// -*- c++ -*-
|
|
2
|
+
#pragma once
|
|
3
|
+
#ifndef __REFLEX_TERMINAL_EXT_DEFS_H__
|
|
4
|
+
#define __REFLEX_TERMINAL_EXT_DEFS_H__
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
#include <rucy.h>
|
|
8
|
+
#include "reflex/defs.h"
|
|
9
|
+
#include "reflex/ruby/defs.h"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
using namespace Xot::Types;
|
|
13
|
+
|
|
14
|
+
using namespace Rucy;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#endif//EOH
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
%w[../xot ../rucy ../rays ../reflex .]
|
|
2
|
+
.map {|s| File.expand_path "../../#{s}/lib", __dir__}
|
|
3
|
+
.each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)}
|
|
4
|
+
|
|
5
|
+
require 'mkmf'
|
|
6
|
+
require 'xot/extconf'
|
|
7
|
+
require 'xot/extension'
|
|
8
|
+
require 'rucy/extension'
|
|
9
|
+
require 'rays/extension'
|
|
10
|
+
require 'reflex/extension'
|
|
11
|
+
require 'reflex-terminal/extension'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Xot::ExtConf.new Xot, Rucy, Rays, Reflex, ReflexTerminal do
|
|
15
|
+
setup do
|
|
16
|
+
vendor = File.expand_path '../../vendor/ghostty', __dir__
|
|
17
|
+
|
|
18
|
+
headers << 'ruby.h'
|
|
19
|
+
defs << 'GHOSTTY_STATIC'
|
|
20
|
+
inc_dirs << "#{vendor}/zig-out/include"
|
|
21
|
+
lib_dirs << "#{vendor}/zig-out/lib"
|
|
22
|
+
local_libs << 'util' if linux?
|
|
23
|
+
local_libs << 'ntdll' if win32?
|
|
24
|
+
local_libs << 'ghostty-vt-static'
|
|
25
|
+
$LDFLAGS << ' -Wl,--out-implib=libreflex-terminal.dll.a' if mingw? || cygwin?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
create_makefile 'reflex_terminal_ext'
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#include "defs.h"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
void Init_reflex_terminal ();
|
|
5
|
+
void Init_reflex_terminal_renderer ();
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
extern "C" void
|
|
9
|
+
Init_reflex_terminal_ext ()
|
|
10
|
+
{
|
|
11
|
+
RUCY_TRY
|
|
12
|
+
|
|
13
|
+
Rucy::init();
|
|
14
|
+
|
|
15
|
+
Init_reflex_terminal();
|
|
16
|
+
Init_reflex_terminal_renderer();
|
|
17
|
+
|
|
18
|
+
RUCY_CATCH
|
|
19
|
+
}
|