-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f8f9f07
commit 0392aa9
Showing
7 changed files
with
535 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.DS_Store | ||
Manifest.toml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
name = "GhosttyExtensions" | ||
uuid = "64489478-8f8d-42d5-80e2-9f7f086a1b9c" | ||
authors = ["Peter Pieske <[email protected]>"] | ||
version = "0.7.0" | ||
|
||
[deps] | ||
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" | ||
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" | ||
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" | ||
TerminalPager = "0c614874-6106-40ed-a7c2-2f1cd0bff883" | ||
|
||
[compat] | ||
TerminalPager = "0.6" | ||
julia = "1.10" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,118 @@ | ||
# GhosttyExtensions.jl | ||
# GhosttyExtensions | ||
|
||
A Julia package that supports some advanced features of the Ghostty terminal emulator. | ||
|
||
> Note: | ||
> | ||
> [Ghostty](https://ghostty.org) will be publicly released on 2024-12-31. | ||
> | ||
> [WezTerm](https://wezfurlong.org/wezterm/index.html) or | ||
> [Kitty](https://sw.kovidgoyal.net/kitty/) should work as well. | ||
All features work over ssh. There's only one external dependency (TerminalPager.jl) and | ||
_TTFP_, the time it adds to the first prompt, should not be noticeable. | ||
|
||
This package requires Julia 1.10 or higher. It has **not** been tested for compatibility with | ||
other packages that alter the REPL e.g., OhMyREPL. | ||
|
||
## Features | ||
|
||
Shell integration: | ||
|
||
- OSC 2 terminal title. | ||
Shows the active project and the remote hostname when connected via ssh. | ||
|
||
- OSC 52 pasteboard support with `pbcopy(x)` and `pbpaste()`. | ||
Copy'n'paste that works over ssh. | ||
|
||
- OSC 133 prompt marking for the prompt modes `julia>`, `shell>`, and `help?>`. | ||
Jump back and forth through the prompts or copy the output between two prompts. | ||
|
||
Inline plotting: | ||
|
||
- Kitty graphics protocol for use with Plots.jl. Plots are shown as PNGs in their original | ||
sizes. There is no scaling. Adjust the plot size within Plots.jl (see below). This | ||
approach makes plots less blurred and there's no need for any dependencies. Use | ||
KittyTerminalImages.jl if you need more features. | ||
|
||
- Inline plotting is automatically activated in interactive sessions and works over ssh. | ||
|
||
- Switch back to the default (e.g., GKSQT.app on macOS) with `inlineplotting(false)`. | ||
Plotting to a GUI app works only on the local machine. | ||
|
||
- In non-interactive scripts, call `inlineplotting()` once to initialize. | ||
Remember to wrap the plot commands with `display(...)`. | ||
|
||
- Get the size of the terminal window in pixels with `pixelsize()`. Invoke `@help pixelsize` | ||
for examples about adjusting the plot size or setting default sizes. | ||
|
||
Extra key bindings: | ||
|
||
- **F1:** call TerminalPager's `@help` for the selection or the word under the cursor | ||
|
||
- **F2:** wrap the buffer in parentheses and move the cursor to the start | ||
|
||
- **F12:** toggle the prefix `@time` | ||
|
||
- **Shift-F12:** toggle the prefix `@code_warntype` | ||
|
||
- **Meta-C:** copy the selection or buffer to the system pasteboard (via OSC 52) | ||
|
||
- **Meta-X:** cut the selection or buffer to the system pasteboard (via OSC 52) | ||
|
||
- **Meta-V:** paste from the system pasteboard and execute (via OSC 52) | ||
|
||
- **Shift-Option-Left, Shift-Option-Right:** select by word | ||
|
||
- **Shift-Command-Left, Shift-Command-Right:** select to the start/end of the line | ||
|
||
- **Shift-Option-Up, Shift-Option-Down:** select to the start/end of the buffer | ||
|
||
Sorry for the Mac specific key bindings. You can change the `extra_keymap` in | ||
`src/GhosttyExtensions.jl`. | ||
|
||
Meta-C and Meta-X can be made much more convenient by binding them to Command-C/Command-X in | ||
Ghostty's `~/.config/ghostty/config`. Currently, we need a separate keybind to copy GUI | ||
selections made with the mouse as we overwrite the default binding: | ||
|
||
``` | ||
keybind = opt+cmd+c=copy_to_clipboard | ||
keybind = cmd+c=esc:C | ||
keybind = cmd+x=esc:X | ||
``` | ||
|
||
Meta-V is intended for automation. For instance, you may want to use AppleScript, | ||
[Hammerspoon](https://hammerspoon.org), or some other app to copy code from your GUI editor | ||
and paste & execute it in Ghostty. You'll be asked for permission to access the system | ||
clipboard or you can opt in permanently in `~/.config/ghostty/config`: | ||
|
||
``` | ||
clipboard-read = allow | ||
``` | ||
|
||
## Installation | ||
|
||
This package is not available in Julia's general registry. It can be added or dev'ed with: | ||
|
||
```julia | ||
Pkg> develop https://github.com/piechologist/GhosttyExtensions.jl | ||
``` | ||
|
||
Add `using GhosttyExtensions` to your `~/.julia/config/startup.jl`. Note that loading | ||
GhosttyExtensions manually after the REPL has been initialized won't work. | ||
|
||
TerminalPager.jl will be installed as a dependency and `@help`, `@out2pr`, | ||
`@stdout_to_pager`, and `pager` re-exported. Please refer to its | ||
[documentation](https://ronisbr.github.io/TerminalPager.jl/stable/). | ||
|
||
## Credits | ||
|
||
- [TerminalExtensions.jl](https://github.com/Keno/TerminalExtensions.jl) | ||
|
||
- [TerminalPager.jl](https://github.com/ronisbr/TerminalPager.jl) | ||
|
||
- [KittyTerminalImages.jl](https://github.com/simonschoelly/KittyTerminalImages.jl) | ||
|
||
- [Kitty terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ||
|
||
- The Julia 1.10 REPL, see `edit(Sys.STDLIB * "/REPL/src/LineEdit.jl")` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
module GhosttyExtensions | ||
|
||
using Base64: base64decode, base64encode | ||
using REPL | ||
using REPL.LineEdit | ||
using TerminalPager | ||
import Base: display | ||
|
||
export TerminalPager, pager, @help, @out2pr, @stdout_to_pager | ||
export inlineplotting, pixelsize | ||
export pbcopy, pbpaste | ||
|
||
include("lineedit.jl") | ||
include("plotting.jl") | ||
include("shellintegration.jl") | ||
|
||
# Extra key bindings. | ||
# | ||
# Tip: the following keys are not used in LineEdit.jl and can be bound to custom functions: | ||
# - Meta + all capital letters except O and W | ||
# - Meta + any of aghijkoqrsvxz | ||
# - ^o and ^v | ||
# | ||
# Note: the history keymap is active when the cursor is at the end of the buffer. It will | ||
# swallow the first part of certain bindings and the remaining part will leak into the | ||
# terminal. We need to add wildcards for these bindings to let them pass through. | ||
# See `LineEdit.prefix_history_keymap` for the default wildcards. | ||
const extra_keymap = Dict{Any,Any}( | ||
"\eOP" => (s, o...) -> invoke_help(s), # F1 | ||
"\eOQ" => (s, o...) -> parenthesize(s), # F2 | ||
"\e[24~" => (s, o...) -> toggle_prefix(s, "@time"), # F12 | ||
"\e[24;2~" => (s, o...) -> toggle_prefix(s, "@code_warntype"), # Shift-F12 | ||
"\eC" => (s, o...) -> copy_region(s), | ||
"\eX" => (s, o...) -> cut_region(s), | ||
"\eV" => (s, o...) -> run_pasteboard(s), | ||
# Shift-Option-Up/Down/Right/Left: | ||
"\e[1;4A" => (s, o...) -> select_to_start_of_buffer(s), | ||
"\e[1;4B" => (s, o...) -> select_to_end_of_buffer(s), | ||
"\e[1;4C" => (s, o...) -> LineEdit.edit_shift_move(s, LineEdit.edit_move_word_right), | ||
"\e[1;4D" => (s, o...) -> LineEdit.edit_shift_move(s, LineEdit.edit_move_word_left), | ||
# Shift-Command-Right/Left: | ||
"\e[1;10C" => (s, o...) -> select_to_end_of_line(s), | ||
"\e[1;10D" => (s, o...) -> select_to_start_of_line(s), | ||
) | ||
|
||
const extra_wildcards = Dict{Any,Any}( | ||
"\e[24~" => "*", # F12 | ||
"\e[24;2~" => "*", # Shift-F12 | ||
"\e[1;4*" => "*", # Shift-Option-ArrowKeys | ||
"\e[1;10*" => "*", # Shift-Command-ArrowKeys | ||
) | ||
|
||
function __init__() | ||
atreplinit() do repl | ||
if isinteractive() && repl isa REPL.LineEditREPL | ||
if isdefined(repl, :interface) | ||
error("another package has already initialized the REPL") | ||
end | ||
|
||
# Set up the REPL with the custom key bindings. | ||
merge!(LineEdit.prefix_history_keymap, extra_wildcards) | ||
repl.interface = REPL.setup_interface(repl; extra_repl_keymap=extra_keymap) | ||
|
||
# Set up the shell integration and KittyDisplay. | ||
shellintegration(repl) | ||
inlineplotting() | ||
end | ||
end | ||
return nothing | ||
end | ||
|
||
end # module GhosttyExtensions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
# ------------------------------------------------------------------------------------------ | ||
# Functions supplementing Julia's REPL.LineEdit.jl | ||
# ------------------------------------------------------------------------------------------ | ||
|
||
"""Copied over from LineEdit.jl. Patched to match words containing @ or . (dot).""" | ||
is_non_word_char(c::Char) = c in """ \t\n\"\\'`\$><=:;|&{}()[],+-*/?%^~""" | ||
|
||
"""Copied over from LineEdit.jl. Patched to use the modified is_non_word_char().""" | ||
function current_word_with_dots(buf) | ||
pos = position(buf) | ||
if eof(buf) || is_non_word_char(peek(buf, Char)) | ||
LineEdit.char_move_word_left(buf, is_non_word_char) | ||
end | ||
LineEdit.char_move_word_right(buf, is_non_word_char) | ||
pend = position(buf) | ||
LineEdit.char_move_word_left(buf, is_non_word_char) | ||
pbegin = position(buf) | ||
word = pend > pbegin ? String(buf.data[(pbegin + 1):pend]) : "" | ||
seek(buf, pos) | ||
return word | ||
end | ||
|
||
"""Copy the selection or the whole buffer to the system pasteboard.""" | ||
function copy_region(s) | ||
if LineEdit.is_region_active(s) | ||
pbcopy(LineEdit.content(s, LineEdit.region(s))) | ||
LineEdit.edit_copy_region(s) | ||
else | ||
pbcopy(LineEdit.content(s)) | ||
end | ||
return nothing | ||
end | ||
|
||
"""Cut the selection or the whole buffer to the system pasteboard.""" | ||
function cut_region(s) | ||
if LineEdit.is_region_active(s) | ||
pbcopy(LineEdit.content(s, LineEdit.region(s))) | ||
LineEdit.edit_kill_region(s) | ||
else | ||
pbcopy(LineEdit.content(s)) | ||
LineEdit.edit_clear(s) | ||
end | ||
return nothing | ||
end | ||
|
||
"""Call `@help` for the selection or the word under the cursor.""" | ||
function invoke_help(s) | ||
mode_name = LineEdit.guess_current_mode_name(s) | ||
if mode_name ≡ :julia | ||
if startswith(LineEdit.content(s), "@help ") | ||
toggle_prefix(s, "@help") | ||
else | ||
if LineEdit.is_region_active(s) | ||
word = LineEdit.content(s, LineEdit.region(s)) | ||
else | ||
word = current_word_with_dots(LineEdit.buffer(s)) | ||
end | ||
if !isempty(word) | ||
LineEdit.edit_clear(s) | ||
write(stdin.buffer, "@help ", word, "\n") | ||
end | ||
end | ||
elseif mode_name ≡ :help | ||
LineEdit.move_input_start(s) | ||
write(stdin.buffer, "\b") | ||
startswith(LineEdit.content(s), "?") && LineEdit.edit_delete(s) | ||
LineEdit.refresh_line(s) | ||
end | ||
return nothing | ||
end | ||
|
||
"""Wrap the whole buffer in parentheses and move the cursor to the start.""" | ||
function parenthesize(s) | ||
LineEdit.move_input_end(s) | ||
LineEdit.edit_insert(s, ')') | ||
LineEdit.move_input_start(s) | ||
LineEdit.edit_insert(s, '(') | ||
LineEdit.move_input_start(s) | ||
LineEdit.refresh_line(s) | ||
return nothing | ||
end | ||
|
||
"""Paste the system pasteboard as bracketed paste and execute it.""" | ||
function run_pasteboard(s) | ||
LineEdit.edit_clear(s) | ||
write(stdin.buffer, "\e[200~", strip(pbpaste()), "\e[201~\n") | ||
return nothing | ||
end | ||
|
||
"""Begin or extend a selection to the of start the buffer.""" | ||
function select_to_start_of_buffer(s) | ||
while position(s) > 0 | ||
LineEdit.edit_shift_move(s, LineEdit.edit_move_word_left) | ||
end | ||
return nothing | ||
end | ||
|
||
"""Begin or extend a selection to the of end the buffer.""" | ||
function select_to_end_of_buffer(s) | ||
while !eof(LineEdit.buffer(s)) | ||
LineEdit.edit_shift_move(s, LineEdit.edit_move_word_right) | ||
end | ||
return nothing | ||
end | ||
|
||
"""Begin or extend a selection to the of start the line.""" | ||
function select_to_start_of_line(s) | ||
while position(s) > 0 | ||
LineEdit.edit_shift_move(s, LineEdit.edit_move_left) | ||
position(s) == 0 && break | ||
LineEdit.buffer(s).data[position(s)] == 0x0a && break | ||
end | ||
return nothing | ||
end | ||
|
||
"""Begin or extend a selection to the of end the line.""" | ||
function select_to_end_of_line(s) | ||
while !eof(LineEdit.buffer(s)) | ||
LineEdit.edit_shift_move(s, LineEdit.edit_move_right) | ||
LineEdit.buffer(s).data[position(s)] == 0x0a && break | ||
end | ||
return nothing | ||
end | ||
|
||
"""Prefix the buffer with `prefix ` or remove it if it's already there.""" | ||
function toggle_prefix(s, prefix) | ||
LineEdit.move_input_start(s) | ||
if startswith(LineEdit.content(s), prefix * " ") | ||
LineEdit.edit_delete_next_word(s) | ||
LineEdit.edit_delete(s) | ||
else | ||
LineEdit.edit_insert(s, prefix * " ") | ||
LineEdit.edit_move_left(s) | ||
end | ||
LineEdit.refresh_line(s) | ||
return nothing | ||
end |
Oops, something went wrong.