mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-16 05:27:50 +02:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08d88af2fb | ||
|
|
4dfbcb539f | ||
|
|
cc0d6621a4 | ||
|
|
d6e55f72c0 | ||
|
|
cd30de3727 | ||
|
|
cec427777c | ||
|
|
30e3ad83bc | ||
|
|
9ef6801f4c | ||
|
|
7f1c371b6e | ||
|
|
2f7b0d1d94 | ||
|
|
90e1ba7781 | ||
|
|
0dfe89a817 | ||
|
|
c76f75a154 | ||
|
|
f51520eb79 | ||
|
|
828f4f312a | ||
|
|
a9c7a85d9a | ||
|
|
38393b50c1 | ||
|
|
7b6c532ac2 | ||
|
|
b3e74de390 | ||
|
|
1aa4d7d24b | ||
|
|
a3e324d623 | ||
|
|
d6116f7426 | ||
|
|
ab9631f045 | ||
|
|
ec0a449c63 | ||
|
|
01ffbfdb42 | ||
|
|
f5621bd56c | ||
|
|
708750173e | ||
|
|
20e43a3e7d | ||
|
|
ff4ee95eba | ||
|
|
2707c44f0f | ||
|
|
e7e401c8dd | ||
|
|
b0ab5bd5eb | ||
|
|
d75395794d | ||
|
|
c7d894d499 | ||
|
|
30905db75f | ||
|
|
89c3b4f9e2 | ||
|
|
0bd50abd77 | ||
|
|
7038292d11 | ||
|
|
b33f8416db | ||
|
|
99b3d0727d | ||
|
|
9503725a32 |
@@ -43,6 +43,21 @@ The :doc:`ssh kitten <kittens/ssh>` is redesigned with powerful new features:
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.32.1 [2024-02-26]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- macOS: Fix a regression in the previous release that broke overriding keyboard shortcuts for actions present in the global menu bar (:iss:`7016`)
|
||||
|
||||
- Fix a regression in the previous release that caused multi-key sequences to not abort when pressing an unknown key (:iss:`7022`)
|
||||
|
||||
- Fix a regression in the previous release that caused `kitten @ launch --cwd=current` to fail over SSH (:iss:`7028`)
|
||||
|
||||
- Fix a regression in the previous release that caused `kitten @ send-text` with a match tab parameter to send text twice to the active window (:iss:`7027`)
|
||||
|
||||
- Fix a regression in the previous release that caused overriding of existing multi-key mappings to fail (:iss:`7044`, :iss:`7058`)
|
||||
|
||||
- Wayland+NVIDIA: Do not request an sRGB output buffer as a bug in Wayland causes kitty to not start (:iss:`7021`)
|
||||
|
||||
0.32.0 [2024-01-19]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ frames-per-second. See below for an overview of all customization possibilities.
|
||||
You can open the config file within kitty by pressing :sc:`edit_config_file`
|
||||
(:kbd:`⌘+,` on macOS). A :file:`kitty.conf` with commented default
|
||||
configurations and descriptions will be created if the file does not exist.
|
||||
You can reload the config file within kitty by pressing :sc:`reload_config_file`
|
||||
(:kbd:`⌃+⌘+,` on macOS) or sending kitty the ``SIGUSR1`` signal.
|
||||
You can also display the current configuration by pressing :sc:`debug_config`
|
||||
(:kbd:`⌥+⌘+,` on macOS).
|
||||
You can reload the config file within kitty by pressing
|
||||
:sc:`reload_config_file` (:kbd:`⌃+⌘+,` on macOS) or sending kitty the
|
||||
``SIGUSR1`` signal with ``kill -SIGUSR1 $KITTY_PID``. You can also display the
|
||||
current configuration by pressing :sc:`debug_config` (:kbd:`⌥+⌘+,` on macOS).
|
||||
|
||||
.. _confloc:
|
||||
|
||||
|
||||
@@ -460,7 +460,10 @@ When you specify a placement id, it will be added to the acknowledgement code
|
||||
above. Every placement is uniquely identified by the pair of the ``image id``
|
||||
and the ``placement id``. If you specify a placement id for an image that does
|
||||
not have an id (i.e. has id=0), it will be ignored. In particular this means
|
||||
there can exist multiple images with ``image id=0, placement id=0``.
|
||||
there can exist multiple images with ``image id=0, placement id=0``. Not
|
||||
specifying a placement id or using ``p=0`` for multiple put commands (``a=p``)
|
||||
with the same non-zero image id results in multiple placements the image.
|
||||
|
||||
An example response::
|
||||
|
||||
<ESC>_Gi=<image id>,p=<placement id>;OK<ESC>\
|
||||
|
||||
@@ -189,13 +189,6 @@ A tool to display weather information in your terminal with curl
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
View and manage the system clipboard under Wayland in your kitty terminal
|
||||
|
||||
.. tool_dmenu_term:
|
||||
|
||||
`dmenu-term <https://github.com/maximbaz/dmenu-term>`_
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Run applications on your system with fuzzy find inside a kitty window
|
||||
|
||||
|
||||
Editor integration
|
||||
-----------------------
|
||||
|
||||
|
||||
@@ -105,9 +105,7 @@ do not. When a key event produces text, the text is sent directly as UTF-8
|
||||
encoded bytes. This is safe as UTF-8 contains no C0 control codes.
|
||||
When the key event does not have text, the key event is encoded as an escape code. In
|
||||
legacy compatibility mode (the default) this uses legacy escape codes, so old terminal
|
||||
applications continue to work. Key events that could not be represented in
|
||||
legacy mode are encoded using a ``CSI u`` escape code, that most terminal
|
||||
programs should just ignore. For more advanced features, such as release/repeat
|
||||
applications continue to work. For more advanced features, such as release/repeat
|
||||
reporting etc., applications can tell the terminal they want this information by
|
||||
sending an escape code to :ref:`progressively enhance <progressive_enhancement>` the data reported for
|
||||
key events.
|
||||
@@ -229,8 +227,10 @@ enhancement <progressive_enhancement>` mechanism described below. Some examples:
|
||||
shift+a -> CSI 97 ; 2 ; 65 u # The text 'A' is reported as 65
|
||||
option+a -> CSI 97 ; ; 229 u # The text 'å' is reported as 229
|
||||
|
||||
If multiple code points are present, they must be separated by colons.
|
||||
If no known key is associated with the text the key number ``0`` must be used.
|
||||
If multiple code points are present, they must be separated by colons. If no
|
||||
known key is associated with the text the key number ``0`` must be used. The
|
||||
associated text must not contain control codes (control codes are code points
|
||||
below U+0020 and codepoints in the C0 and C1 blocks).
|
||||
|
||||
|
||||
Non-Unicode keys
|
||||
@@ -482,6 +482,12 @@ must correspond to the :kbd:`Backspace` key.
|
||||
All keypad keys are reported as their equivalent non-keypad keys. To
|
||||
distinguish these, use the :ref:`disambiguate <disambiguate>` flag.
|
||||
|
||||
Terminals may choose what they want to do about functional keys that have no
|
||||
legacy encoding. kitty chooses to encode these using ``CSI u`` encoding even in
|
||||
legacy mode, so that they become usable even in programs that do not
|
||||
understand the full kitty keyboard protocol. However, terminals may instead choose to
|
||||
ignore such keys in legacy mode instead, or have an option to control this behavior.
|
||||
|
||||
.. _legacy_text:
|
||||
|
||||
Legacy text keys
|
||||
|
||||
@@ -193,12 +193,37 @@ details. A more practical example unmaps the key when the focused window is runn
|
||||
|
||||
map --when-focus-on var:in_editor
|
||||
|
||||
In order to make this work, you need the following lines in your :file:`.vimrc`::
|
||||
In order to make this work, you need to configure your editor as show below:
|
||||
|
||||
let &t_ti = &t_ti . "\\033]1337;SetUserVar=in_editor=MQo\\007"
|
||||
let &t_te = &t_te . "\\033]1337;SetUserVar=in_editor\\007"
|
||||
.. tab:: vim
|
||||
|
||||
These cause vim to set the :code:`in_editor` variable in kitty and unset it when leaving vim.
|
||||
In :file:`~/.vimrc` add:
|
||||
.. code-block:: vim
|
||||
|
||||
let &t_ti = &t_ti . "\\033]1337;SetUserVar=in_editor=MQo\\007"
|
||||
let &t_te = &t_te . "\\033]1337;SetUserVar=in_editor\\007"
|
||||
|
||||
.. tab:: neovim
|
||||
|
||||
In :file:`~/.config/nvim/init.lua` add:
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
vim.api.nvim_create_autocmd({ "VimEnter" }, {
|
||||
group = vim.api.nvim_create_augroup("KittySetVarVimEnter", { clear = true }),
|
||||
callback = function()
|
||||
io.stdout:write("\x1b]1337;SetUserVar=in_editor=MQo\007")
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd({ "VimLeave" }, {
|
||||
group = vim.api.nvim_create_augroup("KittyUnsetVarVimLeave", { clear = true }),
|
||||
callback = function()
|
||||
io.stdout:write("\x1b]1337;SetUserVar=in_editor\007")
|
||||
end,
|
||||
})
|
||||
|
||||
These cause the editor to set the :code:`in_editor` variable in kitty and unset it when exiting.
|
||||
|
||||
Sending arbitrary text or keys to the program running in kitty
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -428,7 +428,7 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) ->
|
||||
# kittens {{{
|
||||
|
||||
@lru_cache
|
||||
def wrapped_kittens() -> Sequence[str]:
|
||||
def wrapped_kittens() -> Tuple[str, ...]:
|
||||
with open('shell-integration/ssh/kitty') as f:
|
||||
for line in f:
|
||||
if line.startswith(' wrapped_kittens="'):
|
||||
@@ -465,7 +465,7 @@ def generate_extra_cli_parser(name: str, spec: str) -> None:
|
||||
|
||||
def kitten_clis() -> None:
|
||||
from kittens.runner import get_kitten_conf_docs, get_kitten_extra_cli_parsers
|
||||
for kitten in wrapped_kittens():
|
||||
for kitten in wrapped_kittens() + ('pager',):
|
||||
defn = get_kitten_conf_docs(kitten)
|
||||
if defn is not None:
|
||||
generate_conf_parser(kitten, defn)
|
||||
|
||||
5
glfw/context.c
vendored
5
glfw/context.c
vendored
@@ -32,7 +32,6 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
|
||||
|
||||
@@ -236,10 +235,10 @@ bool _glfwRefreshContextAttribs(_GLFWwindow* window,
|
||||
}
|
||||
}
|
||||
|
||||
if (!sscanf(version, "%d.%d.%d",
|
||||
if (sscanf(version, "%d.%d.%d",
|
||||
&window->context.major,
|
||||
&window->context.minor,
|
||||
&window->context.revision))
|
||||
&window->context.revision) < 1)
|
||||
{
|
||||
if (window->context.client == GLFW_OPENGL_API)
|
||||
{
|
||||
|
||||
2
glfw/wl_client_side_decorations.c
vendored
2
glfw/wl_client_side_decorations.c
vendored
@@ -76,7 +76,7 @@ blur_mask(kernel_type *image_data, ssize_t width, ssize_t height, ssize_t kernel
|
||||
|
||||
static kernel_type*
|
||||
create_shadow_mask(size_t width, size_t height, size_t margin, size_t kernel_size, kernel_type base_alpha, kernel_type sigma) {
|
||||
kernel_type *mask = calloc(sizeof(kernel_type), 2 * width * height + kernel_size);
|
||||
kernel_type *mask = calloc(2 * width * height + kernel_size, sizeof(kernel_type));
|
||||
if (!mask) return NULL;
|
||||
for (size_t y = margin; y < height - margin; y++) {
|
||||
kernel_type *row = mask + y * width;
|
||||
|
||||
0
kittens/pager/__init__.py
Normal file
0
kittens/pager/__init__.py
Normal file
125
kittens/pager/file_input.go
Normal file
125
kittens/pager/file_input.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// License: GPLv3 Copyright: 2024, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package pager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func wait_for_file_to_grow(file_name string, limit int64) (err error) {
|
||||
// TODO: Use the fsnotify package to avoid this poll
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
s, err := os.Stat(file_name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Size() > limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func read_input(input_file *os.File, input_file_name string, input_channel chan<- input_line_struct, follow bool, count_carriage_returns bool) {
|
||||
const buf_capacity = 8192
|
||||
var buf_array [buf_capacity]byte
|
||||
output_buf := strings.Builder{}
|
||||
output_buf.Grow(buf_capacity)
|
||||
var err error
|
||||
var n int
|
||||
var total_read int64
|
||||
var num_carriage_returns int
|
||||
|
||||
defer func() {
|
||||
_ = input_file.Close()
|
||||
last := input_line_struct{line: output_buf.String(), err: err, num_carriage_returns: num_carriage_returns}
|
||||
if errors.Is(err, io.EOF) {
|
||||
last.err = nil
|
||||
}
|
||||
if len(last.line) > 0 || last.err != nil {
|
||||
input_channel <- last
|
||||
}
|
||||
close(input_channel)
|
||||
}()
|
||||
|
||||
var process_chunk func([]byte)
|
||||
|
||||
if count_carriage_returns {
|
||||
process_chunk = func(chunk []byte) {
|
||||
for _, ch := range chunk {
|
||||
switch ch {
|
||||
case '\r':
|
||||
num_carriage_returns += 1
|
||||
default:
|
||||
_ = output_buf.WriteByte(ch)
|
||||
case '\n':
|
||||
input_channel <- input_line_struct{line: output_buf.String(), num_carriage_returns: num_carriage_returns, is_a_complete_line: true}
|
||||
num_carriage_returns = 0
|
||||
output_buf.Reset()
|
||||
output_buf.Grow(buf_capacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
process_chunk = func(chunk []byte) {
|
||||
for len(chunk) > 0 {
|
||||
idx := bytes.IndexByte(chunk, '\n')
|
||||
switch idx {
|
||||
case -1:
|
||||
_, _ = output_buf.Write(chunk)
|
||||
chunk = nil
|
||||
default:
|
||||
_, _ = output_buf.Write(chunk[idx:])
|
||||
chunk = chunk[idx+1:]
|
||||
input_channel <- input_line_struct{line: output_buf.String(), is_a_complete_line: true}
|
||||
output_buf.Reset()
|
||||
output_buf.Grow(buf_capacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
for err != nil {
|
||||
n, err = input_file.Read(buf_array[:])
|
||||
if n > 0 {
|
||||
total_read += int64(n)
|
||||
process_chunk(buf_array[:n])
|
||||
}
|
||||
if err == unix.EAGAIN || err == unix.EINTR {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if !follow {
|
||||
break
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
input_file.Close()
|
||||
if err = wait_for_file_to_grow(input_file_name, total_read); err != nil {
|
||||
break
|
||||
}
|
||||
if input_file, err = os.Open(input_file_name); err != nil {
|
||||
break
|
||||
}
|
||||
var off int64
|
||||
if off, err = input_file.Seek(total_read, io.SeekStart); err != nil {
|
||||
break
|
||||
}
|
||||
if off != total_read {
|
||||
err = fmt.Errorf("Failed to seek in %s to: %d", input_file_name, off)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
kittens/pager/main.go
Normal file
74
kittens/pager/main.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// License: GPLv3 Copyright: 2024, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package pager
|
||||
|
||||
// TODO:
|
||||
// Scroll to line when starting
|
||||
// Visual mode elect with copy/paste and copy-on-select
|
||||
// Mouse based wheel scroll, drag to select, drag scroll, double click to select
|
||||
// Hyperlinks: Clicking should delegate to terminal and also allow user to specify action
|
||||
// Keyboard hints mode for clicking hyperlinks
|
||||
// Display images when used as scrollback pager
|
||||
// automatic follow when input is a pipe/tty and on last line like tail -f
|
||||
// syntax highlighting using chroma
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"kitty/tools/cli"
|
||||
"kitty/tools/tty"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
var debugprintln = tty.DebugPrintln
|
||||
var _ = debugprintln
|
||||
|
||||
type input_line_struct struct {
|
||||
line string
|
||||
num_carriage_returns int
|
||||
is_a_complete_line bool
|
||||
err error
|
||||
}
|
||||
|
||||
type global_state_struct struct {
|
||||
input_file_name string
|
||||
opts *Options
|
||||
}
|
||||
|
||||
var global_state global_state_struct
|
||||
|
||||
func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) {
|
||||
global_state.opts = opts_
|
||||
input_channel := make(chan input_line_struct, 4096)
|
||||
var input_file *os.File
|
||||
if len(args) > 1 {
|
||||
return 1, fmt.Errorf("Only a single file can be viewed at a time")
|
||||
}
|
||||
if len(args) == 0 {
|
||||
if tty.IsTerminal(os.Stdin.Fd()) {
|
||||
return 1, fmt.Errorf("STDIN is a terminal and no filename specified. See --help")
|
||||
}
|
||||
input_file = os.Stdin
|
||||
global_state.input_file_name = "/dev/stdin"
|
||||
} else {
|
||||
input_file, err = os.Open(args[0])
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
if tty.IsTerminal(input_file.Fd()) {
|
||||
return 1, fmt.Errorf("%s is a terminal not paging it", args[0])
|
||||
}
|
||||
global_state.input_file_name = args[0]
|
||||
}
|
||||
follow := global_state.opts.Follow
|
||||
if follow && global_state.input_file_name == "/dev/stdin" {
|
||||
follow = false
|
||||
}
|
||||
go read_input(input_file, global_state.input_file_name, input_channel, follow, global_state.opts.Role == "scrollback")
|
||||
return
|
||||
}
|
||||
|
||||
func EntryPoint(parent *cli.Command) {
|
||||
create_cmd(parent, main)
|
||||
}
|
||||
42
kittens/pager/main.py
Normal file
42
kittens/pager/main.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from kitty.cli import CompletionSpec
|
||||
|
||||
OPTIONS = '''
|
||||
--role
|
||||
default=pager
|
||||
choices=pager,scrollback
|
||||
The role the pager is used for. The default is a standard less like pager.
|
||||
|
||||
|
||||
--follow
|
||||
type=bool-set
|
||||
Follow changes in the specified file, automatically scrolling if currently on the last line.
|
||||
'''.format
|
||||
|
||||
help_text = '''\
|
||||
Display text in a pager with various features such as searching, copy/paste, etc.
|
||||
Text can some from the specified file or from STDIN. If no filename is specified
|
||||
and STDIN is not a TTY, it is used.
|
||||
'''
|
||||
usage = '[filename]'
|
||||
|
||||
|
||||
def main(args: List[str]) -> None:
|
||||
raise SystemExit('Must be run as kitten pager')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
elif __name__ == '__doc__':
|
||||
cd = sys.cli_docs # type: ignore
|
||||
cd['usage'] = usage
|
||||
cd['options'] = OPTIONS
|
||||
cd['help_text'] = help_text
|
||||
cd['short_desc'] = 'Pretty, side-by-side diffing of files and images'
|
||||
cd['args_completion'] = CompletionSpec.from_string('type:file mime:text/* group:"Text files"')
|
||||
132
kitty/boss.py
132
kitty/boss.py
@@ -91,7 +91,6 @@ from .fast_data_types import (
|
||||
get_options,
|
||||
get_os_window_size,
|
||||
global_font_size,
|
||||
is_modifier_key,
|
||||
last_focused_os_window_id,
|
||||
mark_os_window_for_close,
|
||||
os_window_focus_counters,
|
||||
@@ -105,7 +104,6 @@ from .fast_data_types import (
|
||||
set_application_quit_request,
|
||||
set_background_image,
|
||||
set_boss,
|
||||
set_ignore_os_keyboard_processing,
|
||||
set_options,
|
||||
set_os_window_chrome,
|
||||
set_os_window_size,
|
||||
@@ -117,11 +115,11 @@ from .fast_data_types import (
|
||||
wrapped_kitten_names,
|
||||
)
|
||||
from .key_encoding import get_name_to_functional_number_map
|
||||
from .keys import get_shortcut
|
||||
from .keys import Mappings
|
||||
from .layout.base import set_layout_options
|
||||
from .notify import notification_activated
|
||||
from .options.types import Options
|
||||
from .options.utils import MINIMUM_FONT_SIZE, KeyboardMode, KeyDefinition, KeyMap
|
||||
from .options.utils import MINIMUM_FONT_SIZE, KeyboardMode, KeyDefinition
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .rgb import color_from_int
|
||||
from .session import Session, create_sessions, get_os_window_sizing_data
|
||||
@@ -297,7 +295,7 @@ class VisualSelect:
|
||||
set_os_window_title(self.os_window_id, '')
|
||||
boss = get_boss()
|
||||
redirect_mouse_handling(False)
|
||||
boss.keyboard_mode_stack = []
|
||||
boss.mappings.clear_keyboard_modes()
|
||||
for wid in self.window_ids:
|
||||
w = boss.window_id_map.get(wid)
|
||||
if w is not None:
|
||||
@@ -375,27 +373,11 @@ class Boss:
|
||||
set_boss(self)
|
||||
self.args = args
|
||||
self.mouse_handler: Optional[Callable[[WindowSystemMouseEvent], None]] = None
|
||||
self.keyboard_mode_stack: List[KeyboardMode] = []
|
||||
self.update_keymap(global_shortcuts)
|
||||
self.mappings = Mappings(global_shortcuts)
|
||||
if is_macos:
|
||||
from .fast_data_types import cocoa_set_notification_activated_callback
|
||||
cocoa_set_notification_activated_callback(notification_activated)
|
||||
|
||||
def update_keymap(self, global_shortcuts:Optional[Dict[str, SingleKey]] = None) -> None:
|
||||
if global_shortcuts is None:
|
||||
if is_macos:
|
||||
from .main import set_cocoa_global_shortcuts
|
||||
global_shortcuts = set_cocoa_global_shortcuts(get_options())
|
||||
else:
|
||||
global_shortcuts = {}
|
||||
self.global_shortcuts_map: KeyMap = {v: [KeyDefinition(definition=k)] for k, v in global_shortcuts.items()}
|
||||
self.global_shortcuts = global_shortcuts
|
||||
self.keyboard_modes = get_options().keyboard_modes.copy()
|
||||
km = self.keyboard_modes[''].keymap
|
||||
self.keyboard_modes[''].keymap = km = km.copy()
|
||||
for sc in self.global_shortcuts.values():
|
||||
km.pop(sc, None)
|
||||
|
||||
def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Iterable[Session] = ()) -> None:
|
||||
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
||||
focused_os_window = wid = 0
|
||||
@@ -1341,106 +1323,16 @@ class Boss:
|
||||
End the current keyboard mode switching to the previous mode.
|
||||
''')
|
||||
def pop_keyboard_mode(self) -> bool:
|
||||
passthrough = True
|
||||
if self.keyboard_mode_stack:
|
||||
self.keyboard_mode_stack.pop()
|
||||
if not self.keyboard_mode_stack:
|
||||
set_ignore_os_keyboard_processing(False)
|
||||
passthrough = False
|
||||
return passthrough
|
||||
return self.mappings.pop_keyboard_mode()
|
||||
|
||||
@ac('misc', '''
|
||||
Switch to the specified keyboard mode, pushing it onto the stack of keyboard modes.
|
||||
''')
|
||||
def push_keyboard_mode(self, new_mode: str) -> None:
|
||||
mode = self.keyboard_modes[new_mode]
|
||||
self._push_keyboard_mode(mode)
|
||||
|
||||
def _push_keyboard_mode(self, mode: KeyboardMode) -> None:
|
||||
self.keyboard_mode_stack.append(mode)
|
||||
set_ignore_os_keyboard_processing(True)
|
||||
self.mappings.push_keyboard_mode(new_mode)
|
||||
|
||||
def dispatch_possible_special_key(self, ev: KeyEvent) -> bool:
|
||||
# Handles shortcuts, return True if the key was consumed
|
||||
is_root_mode = not self.keyboard_mode_stack
|
||||
mode = self.keyboard_modes[''] if is_root_mode else self.keyboard_mode_stack[-1]
|
||||
key_action = get_shortcut(mode.keymap, ev)
|
||||
if key_action is None:
|
||||
if is_modifier_key(ev.key):
|
||||
return False
|
||||
if self.global_shortcuts_map and get_shortcut(self.global_shortcuts_map, ev):
|
||||
return True
|
||||
if not is_root_mode:
|
||||
if mode.on_unknown in ('beep', 'ignore'):
|
||||
if mode.on_unknown == 'beep' and get_options().enable_audio_bell:
|
||||
ring_bell()
|
||||
return True
|
||||
if mode.on_unknown == 'passthrough':
|
||||
return False
|
||||
if not self.pop_keyboard_mode():
|
||||
if get_options().enable_audio_bell:
|
||||
ring_bell()
|
||||
return True
|
||||
else:
|
||||
final_actions = self.matching_key_actions(key_action)
|
||||
if final_actions:
|
||||
mode_pos = len(self.keyboard_mode_stack) - 1
|
||||
if final_actions[0].is_sequence:
|
||||
if not mode.is_sequence:
|
||||
sm = KeyboardMode('__sequence__')
|
||||
sm.on_action = 'end'
|
||||
sm.is_sequence = True
|
||||
for fa in final_actions:
|
||||
sm.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy())
|
||||
self._push_keyboard_mode(sm)
|
||||
if self.args.debug_keyboard:
|
||||
print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='', flush=True)
|
||||
else:
|
||||
if len(final_actions) == 1:
|
||||
self.pop_keyboard_mode()
|
||||
return self.combine(final_actions[0].definition)
|
||||
if self.args.debug_keyboard:
|
||||
print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='', flush=True)
|
||||
mode.keymap.clear()
|
||||
for fa in final_actions:
|
||||
mode.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy())
|
||||
return True
|
||||
final_action = final_actions[0]
|
||||
consumed = self.combine(final_action.definition)
|
||||
if consumed and not is_root_mode and mode.on_action == 'end':
|
||||
if mode_pos < len(self.keyboard_mode_stack) and self.keyboard_mode_stack[mode_pos] is mode:
|
||||
del self.keyboard_mode_stack[mode_pos]
|
||||
if not self.keyboard_mode_stack:
|
||||
set_ignore_os_keyboard_processing(False)
|
||||
return consumed
|
||||
return False
|
||||
|
||||
def matching_key_actions(self, candidates: Iterable[KeyDefinition]) -> List[KeyDefinition]:
|
||||
w = self.active_window
|
||||
matches = []
|
||||
has_sequence_match = False
|
||||
for x in candidates:
|
||||
if x.options.when_focus_on:
|
||||
try:
|
||||
if w and w in self.match_windows(x.options.when_focus_on):
|
||||
matches.append(x)
|
||||
if x.is_sequence:
|
||||
has_sequence_match = True
|
||||
except Exception:
|
||||
self.show_error(_('Invalid key mapping'), _(
|
||||
'The match expression {0} is not valid for {1}').format(x.options.when_focus_on, '--when-focus-on'))
|
||||
return []
|
||||
else:
|
||||
if x.is_sequence:
|
||||
has_sequence_match = True
|
||||
matches.append(x)
|
||||
if has_sequence_match:
|
||||
matches = [x for x in matches if x.is_sequence]
|
||||
q = matches[-1].options.when_focus_on
|
||||
matches = [x for x in matches if x.options.when_focus_on == q]
|
||||
else:
|
||||
matches = [matches[-1]]
|
||||
return matches
|
||||
return self.mappings.dispatch_possible_special_key(ev)
|
||||
|
||||
def cancel_current_visual_select(self) -> None:
|
||||
if self.current_visual_select:
|
||||
@@ -1487,7 +1379,7 @@ class Boss:
|
||||
if ch in string.digits:
|
||||
km.keymap[SingleKey(mods=mods, key=fmap[f'KP_{ch}'])].append(ac)
|
||||
if len(self.current_visual_select.window_ids) > 1:
|
||||
self._push_keyboard_mode(km)
|
||||
self.mappings._push_keyboard_mode(km)
|
||||
redirect_mouse_handling(True)
|
||||
self.mouse_handler = self.visual_window_select_mouse_handler
|
||||
else:
|
||||
@@ -1853,7 +1745,11 @@ class Boss:
|
||||
|
||||
cmd = list(map(prepare_arg, get_options().scrollback_pager))
|
||||
if not os.path.isabs(cmd[0]):
|
||||
cmd[0] = which(cmd[0]) or cmd[0]
|
||||
resolved_exe = which(cmd[0])
|
||||
if not resolved_exe:
|
||||
log_error(f'The scrollback_pager {cmd[0]} was not found in PATH, falling back to less')
|
||||
resolved_exe = which('less') or 'less'
|
||||
cmd[0] = resolved_exe
|
||||
|
||||
if os.path.basename(cmd[0]) == 'less':
|
||||
cmd.append('-+F') # reset --quit-if-one-screen
|
||||
@@ -2618,7 +2514,7 @@ class Boss:
|
||||
if is_macos:
|
||||
from .fast_data_types import cocoa_clear_global_shortcuts
|
||||
cocoa_clear_global_shortcuts()
|
||||
self.update_keymap()
|
||||
self.mappings.update_keymap()
|
||||
if is_macos:
|
||||
from .fast_data_types import cocoa_recreate_global_menu
|
||||
cocoa_recreate_global_menu()
|
||||
|
||||
@@ -118,7 +118,10 @@ def finalize_keys(opts: Options, accumulate_bad_lines: Optional[List[BadLine]] =
|
||||
dl = defn.definition_location
|
||||
accumulate_bad_lines.append(BadLine(dl.number, dl.line, KeyError(kerr), dl.file))
|
||||
continue
|
||||
m.keymap[defn.trigger].append(defn)
|
||||
items = m.keymap[defn.trigger]
|
||||
if defn.is_sequence:
|
||||
items = m.keymap[defn.trigger] = [kd for kd in items if defn.rest != kd.rest or defn.options.when_focus_on != kd.options.when_focus_on]
|
||||
items.append(defn)
|
||||
opts.keyboard_modes = modes
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class Version(NamedTuple):
|
||||
|
||||
appname: str = 'kitty'
|
||||
kitty_face = '🐱'
|
||||
version: Version = Version(0, 32, 0)
|
||||
version: Version = Version(0, 32, 1)
|
||||
str_version: str = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
|
||||
@@ -10,7 +10,7 @@ import time
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from pprint import pformat
|
||||
from typing import IO, Callable, Dict, Iterable, Iterator, Optional, Set, TypeVar
|
||||
from typing import IO, Callable, Dict, Iterator, Optional, Sequence, Set, TypeVar
|
||||
|
||||
from kittens.tui.operations import colored, styled
|
||||
|
||||
@@ -59,9 +59,9 @@ def compare_maps(
|
||||
added = set(ef) - set(ei)
|
||||
removed = set(ei) - set(ef)
|
||||
changed = {k for k in set(ef) & set(ei) if ef[k] != ei[k]}
|
||||
which = 'shortcuts' if isinstance(next(iter(initial)), Shortcut) else 'mouse actions'
|
||||
which = 'shortcuts' if isinstance(next(iter(initial or final)), Shortcut) else 'mouse actions'
|
||||
if mode_name and (added or removed or changed):
|
||||
print(f'{title("Changes in keyboard mode: + " + mode_name)}')
|
||||
print(f'{title("Changes in keyboard mode: " + mode_name)}')
|
||||
print_mapping_changes(ef, added, f'Added {which}:', print)
|
||||
print_mapping_changes(ei, removed, f'Removed {which}:', print)
|
||||
print_mapping_changes(ef, changed, f'Changed {which}:', print)
|
||||
@@ -109,8 +109,15 @@ def compare_opts(opts: KittyOpts, print: Print) -> None:
|
||||
return Shortcut((v.trigger,) + v.rest)
|
||||
return Shortcut((k,))
|
||||
|
||||
def as_str(defns: Iterable[KeyDefinition]) -> str:
|
||||
return ', '.join(d.human_repr() for d in defns)
|
||||
def as_str(defns: Sequence[KeyDefinition]) -> str:
|
||||
seen = set()
|
||||
uniq = []
|
||||
for d in reversed(defns):
|
||||
key = d.unique_identity_within_keymap
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
uniq.append(d)
|
||||
return ', '.join(d.human_repr() for d in uniq)
|
||||
|
||||
for kmn, initial_ in default_opts.keyboard_modes.items():
|
||||
initial = {as_sc(k, v[0]): as_str(v) for k, v in initial_.keymap.items()}
|
||||
|
||||
@@ -345,7 +345,7 @@ render_run(RenderCtx *ctx, RenderState *rs) {
|
||||
if (pbm.rows > bm_height) {
|
||||
double ratio = pbm.width / (double)pbm.rows;
|
||||
bm_width = (unsigned)(ratio * bm_height);
|
||||
buf = calloc(sizeof(pixel), (size_t)bm_height * bm_width);
|
||||
buf = calloc((size_t)bm_height * bm_width, sizeof(pixel));
|
||||
if (!buf) break;
|
||||
downsample_32bit_image(pbm.buf, pbm.width, pbm.rows, pbm.stride, buf, bm_width, bm_height);
|
||||
pbm.buf = buf; pbm.stride = 4 * bm_width; pbm.width = bm_width; pbm.rows = bm_height;
|
||||
@@ -442,7 +442,7 @@ render_single_line(FreeTypeRenderCtx ctx_, const char *text, unsigned sz_px, pix
|
||||
if (!hb_buffer_pre_allocate(hb_buffer, 512)) { PyErr_NoMemory(); return false; }
|
||||
|
||||
size_t text_len = strlen(text);
|
||||
char_type *unicode = calloc(sizeof(char_type), text_len + 1);
|
||||
char_type *unicode = calloc(text_len + 1, sizeof(char_type));
|
||||
if (!unicode) { PyErr_NoMemory(); return false; }
|
||||
bool ok = false;
|
||||
text_len = decode_utf8_string(text, text_len, unicode);
|
||||
|
||||
@@ -38,6 +38,10 @@ check_for_gl_error(void UNUSED *ret, const char *name, GLADapiproc UNUSED funcpt
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_nvidia = false;
|
||||
|
||||
bool is_nvidia_gpu_driver(void) { return is_nvidia; }
|
||||
|
||||
void
|
||||
gl_init(void) {
|
||||
static bool glad_loaded = false;
|
||||
@@ -59,7 +63,9 @@ gl_init(void) {
|
||||
glad_loaded = true;
|
||||
int gl_major = GLAD_VERSION_MAJOR(gl_version);
|
||||
int gl_minor = GLAD_VERSION_MINOR(gl_version);
|
||||
if (global_state.debug_rendering) printf("GL version string: '%s' Detected version: %d.%d\n", glGetString(GL_VERSION), gl_major, gl_minor);
|
||||
const char *gvs = (const char*)glGetString(GL_VERSION);
|
||||
if (strstr(gvs, "NVIDIA")) is_nvidia = true;
|
||||
if (global_state.debug_rendering) printf("GL version string: '%s' Detected version: %d.%d\n", gvs, gl_major, gl_minor);
|
||||
if (gl_major < OPENGL_REQUIRED_VERSION_MAJOR || (gl_major == OPENGL_REQUIRED_VERSION_MAJOR && gl_minor < OPENGL_REQUIRED_VERSION_MINOR)) {
|
||||
fatal("OpenGL version is %d.%d, version >= 3.3 required for kitty", gl_major, gl_minor);
|
||||
}
|
||||
|
||||
@@ -56,3 +56,4 @@ void bind_vao_uniform_buffer(ssize_t vao_idx, size_t bufnum, GLuint block_index)
|
||||
void unbind_vertex_array(void);
|
||||
void unbind_program(void);
|
||||
GLuint compile_shaders(GLenum shader_type, GLsizei count, const GLchar * const * string);
|
||||
bool is_nvidia_gpu_driver(void);
|
||||
|
||||
@@ -1075,7 +1075,9 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
|
||||
glfwSetIMECursorPositionCallback(get_ime_cursor_position);
|
||||
glfwSetSystemColorThemeChangeCallback(on_system_color_scheme_change);
|
||||
// Request SRGB output buffer
|
||||
glfwWindowHint(GLFW_SRGB_CAPABLE, true);
|
||||
// Prevents kitty from starting on Wayland + NVIDIA, sigh: https://github.com/kovidgoyal/kitty/issues/7021
|
||||
// Remove after https://github.com/NVIDIA/egl-wayland/issues/85 is fixed.
|
||||
if (!global_state.is_wayland || !is_nvidia_gpu_driver()) glfwWindowHint(GLFW_SRGB_CAPABLE, true);
|
||||
#ifdef __APPLE__
|
||||
cocoa_set_activation_policy(OPT(macos_hide_from_tasks));
|
||||
glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, true);
|
||||
|
||||
@@ -392,7 +392,7 @@ static void
|
||||
pagerhist_rewrap_to(HistoryBuf *self, index_type cells_in_line) {
|
||||
PagerHistoryBuf *ph = self->pagerhist;
|
||||
if (!ph->ringbuf || !ringbuf_bytes_used(ph->ringbuf)) return;
|
||||
PagerHistoryBuf *nph = calloc(sizeof(PagerHistoryBuf), 1);
|
||||
PagerHistoryBuf *nph = calloc(1, sizeof(PagerHistoryBuf));
|
||||
if (!nph) return;
|
||||
nph->maximum_size = ph->maximum_size;
|
||||
nph->ringbuf = ringbuf_new(MIN(ph->maximum_size, ringbuf_capacity(ph->ringbuf) + 4096));
|
||||
|
||||
194
kitty/keys.py
194
kitty/keys.py
@@ -1,12 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import List, Optional
|
||||
from gettext import gettext as _
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional
|
||||
|
||||
from .fast_data_types import GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_HYPER, GLFW_MOD_META, GLFW_MOD_SHIFT, GLFW_MOD_SUPER, KeyEvent, SingleKey
|
||||
from .options.utils import KeyDefinition, KeyMap
|
||||
from .constants import is_macos
|
||||
from .fast_data_types import (
|
||||
GLFW_MOD_ALT,
|
||||
GLFW_MOD_CONTROL,
|
||||
GLFW_MOD_HYPER,
|
||||
GLFW_MOD_META,
|
||||
GLFW_MOD_SHIFT,
|
||||
GLFW_MOD_SUPER,
|
||||
KeyEvent,
|
||||
SingleKey,
|
||||
get_boss,
|
||||
get_options,
|
||||
is_modifier_key,
|
||||
ring_bell,
|
||||
set_ignore_os_keyboard_processing,
|
||||
)
|
||||
from .options.types import Options
|
||||
from .options.utils import KeyboardMode, KeyDefinition, KeyMap
|
||||
from .typing import ScreenType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .window import Window
|
||||
|
||||
mod_mask = GLFW_MOD_ALT | GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER | GLFW_MOD_META | GLFW_MOD_HYPER
|
||||
|
||||
|
||||
@@ -37,3 +57,171 @@ def shortcut_matches(s: SingleKey, ev: KeyEvent) -> bool:
|
||||
if ev.shifted_key and mods & GLFW_MOD_SHIFT and (mods & ~GLFW_MOD_SHIFT) == smods and ev.shifted_key == s.key:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Mappings:
|
||||
|
||||
' Manage all keyboard mappings '
|
||||
|
||||
def __init__(self, global_shortcuts:Optional[Dict[str, SingleKey]] = None) -> None:
|
||||
self.keyboard_mode_stack: List[KeyboardMode] = []
|
||||
self.update_keymap(global_shortcuts)
|
||||
|
||||
def update_keymap(self, global_shortcuts:Optional[Dict[str, SingleKey]] = None) -> None:
|
||||
if global_shortcuts is None:
|
||||
global_shortcuts = self.set_cocoa_global_shortcuts(self.get_options()) if is_macos else {}
|
||||
self.global_shortcuts_map: KeyMap = {v: [KeyDefinition(definition=k)] for k, v in global_shortcuts.items()}
|
||||
self.global_shortcuts = global_shortcuts
|
||||
self.keyboard_modes = self.get_options().keyboard_modes.copy()
|
||||
km = self.keyboard_modes[''].keymap
|
||||
self.keyboard_modes[''].keymap = km = km.copy()
|
||||
for sc in self.global_shortcuts.values():
|
||||
km.pop(sc, None)
|
||||
|
||||
def clear_keyboard_modes(self) -> None:
|
||||
self.keyboard_mode_stack = []
|
||||
|
||||
def pop_keyboard_mode(self) -> bool:
|
||||
passthrough = True
|
||||
if self.keyboard_mode_stack:
|
||||
self.keyboard_mode_stack.pop()
|
||||
if not self.keyboard_mode_stack:
|
||||
self.set_ignore_os_keyboard_processing(False)
|
||||
passthrough = False
|
||||
return passthrough
|
||||
|
||||
def _push_keyboard_mode(self, mode: KeyboardMode) -> None:
|
||||
self.keyboard_mode_stack.append(mode)
|
||||
self.set_ignore_os_keyboard_processing(True)
|
||||
|
||||
def push_keyboard_mode(self, new_mode: str) -> None:
|
||||
mode = self.keyboard_modes[new_mode]
|
||||
self._push_keyboard_mode(mode)
|
||||
|
||||
def matching_key_actions(self, candidates: Iterable[KeyDefinition]) -> List[KeyDefinition]:
|
||||
w = self.get_active_window()
|
||||
matches = []
|
||||
has_sequence_match = False
|
||||
for x in candidates:
|
||||
if x.options.when_focus_on:
|
||||
try:
|
||||
if w and w in self.match_windows(x.options.when_focus_on):
|
||||
matches.append(x)
|
||||
if x.is_sequence:
|
||||
has_sequence_match = True
|
||||
except Exception:
|
||||
self.show_error(_('Invalid key mapping'), _(
|
||||
'The match expression {0} is not valid for {1}').format(x.options.when_focus_on, '--when-focus-on'))
|
||||
return []
|
||||
else:
|
||||
if x.is_sequence:
|
||||
has_sequence_match = True
|
||||
matches.append(x)
|
||||
if has_sequence_match:
|
||||
terminal_matches = [x for x in matches if not x.rest]
|
||||
if terminal_matches:
|
||||
matches = [terminal_matches[-1]]
|
||||
else:
|
||||
matches = [x for x in matches if x.is_sequence]
|
||||
q = matches[-1].options.when_focus_on
|
||||
matches = [x for x in matches if x.options.when_focus_on == q]
|
||||
else:
|
||||
matches = [matches[-1]]
|
||||
return matches
|
||||
|
||||
def dispatch_possible_special_key(self, ev: KeyEvent) -> bool:
|
||||
# Handles shortcuts, return True if the key was consumed
|
||||
is_root_mode = not self.keyboard_mode_stack
|
||||
mode = self.keyboard_modes[''] if is_root_mode else self.keyboard_mode_stack[-1]
|
||||
key_action = get_shortcut(mode.keymap, ev)
|
||||
if key_action is None:
|
||||
if is_modifier_key(ev.key):
|
||||
return False
|
||||
if self.global_shortcuts_map and get_shortcut(self.global_shortcuts_map, ev):
|
||||
return True
|
||||
if not is_root_mode:
|
||||
if mode.sequence_keys is not None:
|
||||
self.pop_keyboard_mode()
|
||||
w = self.get_active_window()
|
||||
if w is not None:
|
||||
w.send_key_sequence(*mode.sequence_keys)
|
||||
return False
|
||||
if mode.on_unknown in ('beep', 'ignore'):
|
||||
if mode.on_unknown == 'beep':
|
||||
self.ring_bell()
|
||||
return True
|
||||
if mode.on_unknown == 'passthrough':
|
||||
return False
|
||||
if not self.pop_keyboard_mode():
|
||||
self.ring_bell()
|
||||
return True
|
||||
else:
|
||||
final_actions = self.matching_key_actions(key_action)
|
||||
if final_actions:
|
||||
mode_pos = len(self.keyboard_mode_stack) - 1
|
||||
if final_actions[0].is_sequence:
|
||||
if mode.sequence_keys is None:
|
||||
sm = KeyboardMode('__sequence__')
|
||||
sm.on_action = 'end'
|
||||
sm.sequence_keys = [ev]
|
||||
for fa in final_actions:
|
||||
sm.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy())
|
||||
self._push_keyboard_mode(sm)
|
||||
self.debug_print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='')
|
||||
else:
|
||||
if len(final_actions) == 1:
|
||||
self.pop_keyboard_mode()
|
||||
consumed = self.combine(final_actions[0].definition)
|
||||
if not consumed:
|
||||
w = self.get_active_window()
|
||||
if w is not None:
|
||||
w.send_key_sequence(*mode.sequence_keys)
|
||||
return consumed
|
||||
mode.sequence_keys.append(ev)
|
||||
self.debug_print('\n\x1b[35mKeyPress\x1b[m matched sequence prefix, ', end='')
|
||||
mode.keymap.clear()
|
||||
for fa in final_actions:
|
||||
mode.keymap[fa.rest[0]].append(fa.shift_sequence_and_copy())
|
||||
return True
|
||||
final_action = final_actions[0]
|
||||
consumed = self.combine(final_action.definition)
|
||||
if consumed and not is_root_mode and mode.on_action == 'end':
|
||||
if mode_pos < len(self.keyboard_mode_stack) and self.keyboard_mode_stack[mode_pos] is mode:
|
||||
del self.keyboard_mode_stack[mode_pos]
|
||||
if not self.keyboard_mode_stack:
|
||||
self.set_ignore_os_keyboard_processing(False)
|
||||
return consumed
|
||||
return False
|
||||
|
||||
# System integration {{{
|
||||
def get_active_window(self) -> Optional['Window']:
|
||||
return get_boss().active_window
|
||||
|
||||
def match_windows(self, expr: str) -> Iterator['Window']:
|
||||
return get_boss().match_windows(expr)
|
||||
|
||||
def show_error(self, title: str, msg: str) -> None:
|
||||
return get_boss().show_error(title, msg)
|
||||
|
||||
def ring_bell(self) -> None:
|
||||
if self.get_options().enable_audio_bell:
|
||||
ring_bell()
|
||||
|
||||
def combine(self, action_definition: str) -> bool:
|
||||
return get_boss().combine(action_definition)
|
||||
|
||||
def set_ignore_os_keyboard_processing(self, on: bool) -> None:
|
||||
set_ignore_os_keyboard_processing(on)
|
||||
|
||||
def get_options(self) -> Options:
|
||||
return get_options()
|
||||
|
||||
def debug_print(self, *args: Any, end: str = '\n') -> None:
|
||||
b = get_boss()
|
||||
if b.args.debug_keyboard:
|
||||
print(*args, end=end, flush=True)
|
||||
|
||||
def set_cocoa_global_shortcuts(self, opts: Options) -> Dict[str, SingleKey]:
|
||||
from .main import set_cocoa_global_shortcuts
|
||||
return set_cocoa_global_shortcuts(opts)
|
||||
# }}}
|
||||
|
||||
@@ -219,11 +219,11 @@ def set_cocoa_global_shortcuts(opts: Options) -> Dict[str, SingleKey]:
|
||||
if is_macos:
|
||||
from collections import defaultdict
|
||||
func_map = defaultdict(list)
|
||||
for k, v in opts.keyboard_modes[''].keymap.items():
|
||||
for kd in v:
|
||||
if kd.is_suitable_for_global_shortcut:
|
||||
parts = tuple(kd.definition.split())
|
||||
func_map[parts].append(k)
|
||||
for single_key, v in opts.keyboard_modes[''].keymap.items():
|
||||
kd = v[-1] # the last definition is the active one
|
||||
if kd.is_suitable_for_global_shortcut:
|
||||
parts = tuple(kd.definition.split())
|
||||
func_map[parts].append(single_key)
|
||||
|
||||
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab',
|
||||
'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry', 'toggle_fullscreen',
|
||||
|
||||
@@ -534,7 +534,7 @@ dispatch_possible_click(Window *w, int button, int modifiers) {
|
||||
Screen *screen = w->render_data.screen;
|
||||
int count = multi_click_count(w, button);
|
||||
if (release_is_click(w, button)) {
|
||||
PendingClick *pc = calloc(sizeof(PendingClick), 1);
|
||||
PendingClick *pc = calloc(1, sizeof(PendingClick));
|
||||
if (pc) {
|
||||
const ClickQueue *q = &w->click_queues[button];
|
||||
pc->press_num = q->length ? q->clicks[q->length - 1].num : 0;
|
||||
|
||||
@@ -1207,6 +1207,14 @@ class KeyDefinition(BaseDefinition):
|
||||
def is_suitable_for_global_shortcut(self) -> bool:
|
||||
return not self.options.when_focus_on and not self.options.mode and not self.options.new_mode and not self.is_sequence
|
||||
|
||||
@property
|
||||
def full_key_sequence_to_trigger(self) -> Tuple[SingleKey, ...]:
|
||||
return (self.trigger,) + self.rest
|
||||
|
||||
@property
|
||||
def unique_identity_within_keymap(self) -> Tuple[Tuple[SingleKey, ...], str]:
|
||||
return self.full_key_sequence_to_trigger, self.options.when_focus_on
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.pretty_repr('is_sequence', 'trigger', 'rest', 'options')
|
||||
|
||||
@@ -1234,7 +1242,7 @@ class KeyboardMode:
|
||||
|
||||
on_unknown: OnUnknown = get_args(OnUnknown)[0]
|
||||
on_action : OnAction = get_args(OnAction)[0]
|
||||
is_sequence: bool = False
|
||||
sequence_keys: Optional[List[defines.KeyEvent]] = None
|
||||
|
||||
def __init__(self, name: str = '') -> None:
|
||||
self.name = name
|
||||
|
||||
@@ -392,6 +392,7 @@ class RemoteCommand:
|
||||
tabs = tuple(boss.match_tabs(payload_get(tab_match_name)))
|
||||
if not tabs:
|
||||
raise MatchError(payload_get(tab_match_name), 'tabs')
|
||||
windows = []
|
||||
for tab in tabs:
|
||||
windows += list(tab)
|
||||
return windows
|
||||
|
||||
@@ -1048,7 +1048,7 @@ draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_bu
|
||||
|
||||
static bool
|
||||
attach_shaders(PyObject *sources, GLuint program_id, GLenum shader_type) {
|
||||
RAII_ALLOC(const GLchar*, c_sources, calloc(sizeof(GLchar*), PyTuple_GET_SIZE(sources)));
|
||||
RAII_ALLOC(const GLchar*, c_sources, calloc(PyTuple_GET_SIZE(sources), sizeof(GLchar*)));
|
||||
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(sources); i++) {
|
||||
PyObject *temp = PyTuple_GET_ITEM(sources, i);
|
||||
if (!PyUnicode_Check(temp)) { PyErr_SetString(PyExc_TypeError, "shaders must be strings"); return false; }
|
||||
|
||||
@@ -178,8 +178,8 @@ class CwdRequest:
|
||||
env.pop(k, None)
|
||||
for k in (
|
||||
'HOME', 'USER', 'TEMP', 'TMP', 'TMPDIR', 'PATH', 'PWD', 'OLDPWD', 'KITTY_INSTALLATION_DIR',
|
||||
'HOSTNAME', 'SSH_AUTH_SOCK', 'SSH_AGENT_PID', 'KITTY_WINDOW_ID', 'KITTY_STDIO_FORWARDED',
|
||||
'KITTY_PID', 'KITTY_PUBLIC_KEY', 'KITTY_WINDOW_ID', 'TERMINFO', 'XDG_RUNTIME_DIR', 'XDG_VTNR',
|
||||
'HOSTNAME', 'SSH_AUTH_SOCK', 'SSH_AGENT_PID', 'KITTY_STDIO_FORWARDED',
|
||||
'KITTY_PUBLIC_KEY', 'TERMINFO', 'XDG_RUNTIME_DIR', 'XDG_VTNR',
|
||||
'XDG_DATA_DIRS', 'XAUTHORITY', 'EDITOR', 'VISUAL',
|
||||
):
|
||||
env.pop(k, None)
|
||||
@@ -913,6 +913,17 @@ class Window:
|
||||
passthrough = False
|
||||
return passthrough
|
||||
|
||||
def send_key_sequence(self, *keys: KeyEvent, synthesize_release_events: bool = True) -> None:
|
||||
for key in keys:
|
||||
enc = self.encoded_key(key)
|
||||
if enc:
|
||||
self.write_to_child(enc)
|
||||
if synthesize_release_events and key.action != GLFW_RELEASE:
|
||||
rkey = KeyEvent(key=key.key, mods=key.mods, action=GLFW_RELEASE)
|
||||
enc = self.encoded_key(rkey)
|
||||
if enc:
|
||||
self.write_to_child(enc)
|
||||
|
||||
@ac('debug', 'Show a dump of the current lines in the scrollback + screen with their line attributes')
|
||||
def dump_lines_with_attrs(self) -> None:
|
||||
strings: List[str] = []
|
||||
|
||||
@@ -5,6 +5,7 @@ from functools import partial
|
||||
|
||||
import kitty.fast_data_types as defines
|
||||
from kitty.key_encoding import EventType, KeyEvent, decode_key_event, encode_key_event
|
||||
from kitty.keys import Mappings
|
||||
|
||||
from . import BaseTest
|
||||
|
||||
@@ -504,3 +505,119 @@ class TestKeys(BaseTest):
|
||||
self.ae(enc(mods=defines.GLFW_MOD_SHIFT), '<4;1;1M')
|
||||
self.ae(enc(mods=defines.GLFW_MOD_ALT), '<8;1;1M')
|
||||
self.ae(enc(mods=defines.GLFW_MOD_CONTROL), '<16;1;1M')
|
||||
|
||||
def test_mapping(self):
|
||||
from kitty.config import load_config
|
||||
from kitty.options.utils import parse_shortcut
|
||||
af = self.assertFalse
|
||||
|
||||
class Window:
|
||||
def __init__(self, id=1):
|
||||
self.key_seqs = []
|
||||
self.id = id
|
||||
|
||||
def send_key_sequence(self, *s):
|
||||
self.key_seqs.extend(s)
|
||||
|
||||
class TM(Mappings):
|
||||
|
||||
def __init__(self, *lines, active_window = Window()):
|
||||
self.active_window = active_window
|
||||
self.windows = [active_window]
|
||||
bad_lines = []
|
||||
self.options = load_config(overrides=lines, accumulate_bad_lines=bad_lines)
|
||||
af(bad_lines)
|
||||
super().__init__()
|
||||
|
||||
def get_active_window(self):
|
||||
return self.active_window
|
||||
|
||||
def match_windows(self, expr: str):
|
||||
for w in self.windows:
|
||||
if str(w.id) == expr:
|
||||
yield w
|
||||
|
||||
def show_error(self, title: str, msg: str) -> None:
|
||||
pass
|
||||
|
||||
def ring_bell(self) -> None:
|
||||
pass
|
||||
|
||||
def debug_print(self, *args, end: str = '\n') -> None:
|
||||
pass
|
||||
|
||||
def combine(self, action_definition: str) -> bool:
|
||||
self.actions.append(action_definition)
|
||||
if action_definition.startswith('push_keyboard_mode '):
|
||||
self.push_keyboard_mode(action_definition.partition(' ')[2])
|
||||
elif action_definition == 'pop_keyboard_mode':
|
||||
self.pop_keyboard_mode()
|
||||
return bool(action_definition)
|
||||
|
||||
def set_ignore_os_keyboard_processing(self, on: bool) -> None:
|
||||
pass
|
||||
|
||||
def set_cocoa_global_shortcuts(self, opts):
|
||||
return {}
|
||||
|
||||
def get_options(self):
|
||||
return self.options
|
||||
|
||||
def __call__(self, *keys: str):
|
||||
self.actions = []
|
||||
self.active_window.key_seqs = []
|
||||
consumed = []
|
||||
for key in keys:
|
||||
sk = parse_shortcut(key)
|
||||
ev = defines.KeyEvent(sk.key, 0, 0, sk.mods)
|
||||
consumed.append(self.dispatch_possible_special_key(ev))
|
||||
return consumed
|
||||
|
||||
tm = TM('map ctrl+a new_window_with_cwd')
|
||||
self.ae(tm('ctrl+a'), [True])
|
||||
self.ae(tm.actions, ['new_window_with_cwd'])
|
||||
|
||||
tm = TM('map ctrl+f>2 set_font_size 20')
|
||||
self.ae(tm('ctrl+f', '2'), [True, True])
|
||||
self.ae(tm.actions, ['set_font_size 20'])
|
||||
af(tm.active_window.key_seqs)
|
||||
# unmatched multi key mapping should send all keys to child
|
||||
self.ae(tm('ctrl+f', '1'), [True, False])
|
||||
af(tm.actions)
|
||||
self.ae(len(tm.active_window.key_seqs), 1) # ctrl+f should have been sent to the window
|
||||
# multi-key mapping that is unmapped should send all keys to child
|
||||
tm = TM('map kitty_mod+p>f')
|
||||
self.ae(tm('ctrl+shift+p', 'f'), [True, False])
|
||||
self.ae(len(tm.active_window.key_seqs), 1)
|
||||
|
||||
# unmap
|
||||
tm = TM('map kitty_mod+enter')
|
||||
self.ae(tm('ctrl+shift+enter'), [False])
|
||||
|
||||
# single key mapping overrides all multi-key mappings with same prefix
|
||||
tm = TM('map kitty_mod+p new_window')
|
||||
self.ae(tm('ctrl+shift+p', 'f'), [True, False])
|
||||
self.ae(tm.actions, ['new_window'])
|
||||
|
||||
# changing a multi key mapping
|
||||
tm = TM('map kitty_mod+p>f new_window')
|
||||
self.ae(tm('ctrl+shift+p', 'f'), [True, True])
|
||||
self.ae(tm.actions, ['new_window'])
|
||||
|
||||
# different behavior with focus selection
|
||||
tm = TM('map --when-focus-on 2 kitty_mod+t')
|
||||
tm.windows.append(Window(2))
|
||||
self.ae(tm('ctrl+shift+t'), [True])
|
||||
tm.active_window = tm.windows[1]
|
||||
self.ae(tm('ctrl+shift+t'), [False])
|
||||
|
||||
# modal mappings
|
||||
tm = TM('map --new-mode mw --on-unknown end kitty_mod+f7', 'map --mode mw left neighboring_window left', 'map --mode mw right neighboring_window right')
|
||||
self.ae(tm('ctrl+shift+f7'), [True])
|
||||
self.ae(tm.actions, ['push_keyboard_mode mw'])
|
||||
self.ae(tm('right'), [True])
|
||||
self.ae(tm.actions, ['neighboring_window right'])
|
||||
self.ae(tm('left'), [True])
|
||||
self.ae(tm.actions, ['neighboring_window left'])
|
||||
self.ae(tm('x'), [True])
|
||||
af(tm.keyboard_mode_stack)
|
||||
|
||||
13
setup.py
13
setup.py
@@ -454,6 +454,11 @@ def init_env(
|
||||
# Universal build fails with -fcf-protection clang is not smart enough to filter it out for the ARM part
|
||||
intel_control_flow_protection = '-fcf-protection=full' if ccver >= (9, 0) and not build_universal_binary else ''
|
||||
control_flow_protection = arm_control_flow_protection if is_arm else intel_control_flow_protection
|
||||
env_cflags = shlex.split(os.environ.get('CFLAGS', ''))
|
||||
env_cppflags = shlex.split(os.environ.get('CPPFLAGS', ''))
|
||||
env_ldflags = shlex.split(os.environ.get('LDFLAGS', ''))
|
||||
if control_flow_protection and not test_compile(cc, control_flow_protection, *env_cppflags, *env_cflags, ldflags=env_ldflags):
|
||||
control_flow_protection = ''
|
||||
cflags_ = os.environ.get(
|
||||
'OVERRIDE_CFLAGS', (
|
||||
f'-Wextra {float_conversion} -Wno-missing-field-initializers -Wall -Wstrict-prototypes {std}'
|
||||
@@ -470,9 +475,9 @@ def init_env(
|
||||
)
|
||||
ldflags = shlex.split(ldflags_)
|
||||
ldflags.append('-shared')
|
||||
cppflags += shlex.split(os.environ.get('CPPFLAGS', ''))
|
||||
cflags += shlex.split(os.environ.get('CFLAGS', ''))
|
||||
ldflags += shlex.split(os.environ.get('LDFLAGS', ''))
|
||||
cppflags += env_cppflags
|
||||
cflags += env_cflags
|
||||
ldflags += env_ldflags
|
||||
if not debug and not sanitize and not is_openbsd and link_time_optimization:
|
||||
# See https://github.com/google/sanitizers/issues/647
|
||||
cflags.append('-flto')
|
||||
@@ -971,7 +976,7 @@ def build_static_kittens(
|
||||
go = shutil.which('go')
|
||||
if not go:
|
||||
raise SystemExit('The go tool was not found on this system. Install Go')
|
||||
required_go_version = subprocess.check_output([go] + 'list -f {{.GoVersion}} -m'.split()).decode().strip()
|
||||
required_go_version = subprocess.check_output([go] + 'list -f {{.GoVersion}} -m'.split(), env=dict(os.environ, GO111MODULE="on")).decode().strip()
|
||||
current_go_version = subprocess.check_output([go, 'version']).decode().strip().split()[2][2:]
|
||||
if parse_go_version(required_go_version) > parse_go_version(current_go_version):
|
||||
raise SystemExit(f'The version of go on this system ({current_go_version}) is too old. go >= {required_go_version} is needed')
|
||||
|
||||
@@ -214,16 +214,16 @@ def get_data():
|
||||
tf.extractall(tdir)
|
||||
with open(tdir + '/data.sh') as f:
|
||||
env_vars = f.read()
|
||||
apply_env_vars(env_vars)
|
||||
data_dir = os.environ.pop('KITTY_SSH_KITTEN_DATA_DIR')
|
||||
if not os.path.isabs(data_dir):
|
||||
data_dir = os.path.join(HOME, data_dir)
|
||||
data_dir = os.path.abspath(data_dir)
|
||||
shell_integration_dir = os.path.join(data_dir, 'shell-integration')
|
||||
compile_terminfo(tdir + '/home')
|
||||
move(tdir + '/home', HOME)
|
||||
if os.path.exists(tdir + '/root'):
|
||||
move(tdir + '/root', '/')
|
||||
apply_env_vars(env_vars)
|
||||
data_dir = os.environ.pop('KITTY_SSH_KITTEN_DATA_DIR')
|
||||
if not os.path.isabs(data_dir):
|
||||
data_dir = os.path.join(HOME, data_dir)
|
||||
data_dir = os.path.abspath(data_dir)
|
||||
shell_integration_dir = os.path.join(data_dir, 'shell-integration')
|
||||
compile_terminfo(tdir + '/home')
|
||||
move(tdir + '/home', HOME)
|
||||
if os.path.exists(tdir + '/root'):
|
||||
move(tdir + '/root', '/')
|
||||
|
||||
|
||||
def exec_zsh_with_integration():
|
||||
|
||||
Reference in New Issue
Block a user