Compare commits

..

40 Commits

Author SHA1 Message Date
Kovid Goyal
5bdf27fd78 version 0.26.2 2022-09-05 10:49:50 +05:30
Kovid Goyal
f05783e64d Sanitize notifications ids as they are retransmitted over the TTY 2022-09-05 10:41:19 +05:30
Kovid Goyal
c455fea729 Also output screen contents when test fails 2022-09-05 07:52:36 +05:30
Kovid Goyal
5bf5454dbe Nicer fix for #5469 2022-09-04 22:15:17 +05:30
Kovid Goyal
b60d9f73ce Wayland: Fix remembering window size not accurate when client side decorations are present 2022-09-04 14:25:04 +05:30
Kovid Goyal
31fe35cd38 Fix #5469 2022-09-04 13:58:16 +05:30
Kovid Goyal
607bc845fc Allow creating overlay-main windows, which are treated as the active window unlike normal overlays
Fixes #5392
2022-09-03 19:04:31 +05:30
Kovid Goyal
a420d9d19c spacing is a clone safe option 2022-09-02 23:28:58 +05:30
Kovid Goyal
4b81e4936e launch: Allow setting the margin and padding for the newly created window
Fixes #5463
2022-09-02 19:41:05 +05:30
Kovid Goyal
295743a96e Fix ref to launch doc 2022-09-02 16:31:21 +05:30
Kovid Goyal
d3a1848c27 Remove markup from ref_hyperlink text 2022-09-02 16:24:50 +05:30
Kovid Goyal
9cec924f32 hints kitten: Allow using launch as the program to run, to open the result in a new kitty tab/window/etc.
Fixes #5462
2022-09-02 16:23:00 +05:30
Kovid Goyal
67c963ab89 Pass KITTY_LISTEN_ON to programs opened via open_url
Useful if those programs want to talk to kitty via the rc socket
2022-09-02 16:01:30 +05:30
Kovid Goyal
0f7ced25c1 ... 2022-09-01 13:55:48 +05:30
Kovid Goyal
42948ffff0 Merge branch 'docs' of https://github.com/page-down/kitty 2022-09-01 13:55:30 +05:30
pagedown
922ce5bdea Docs: Put example commands in code blocks 2022-09-01 15:35:51 +08:00
pagedown
1b42486026 Docs: Remove the extra empty line in the CLI help
Also keep the example code in a code block, in the HTML documentation.
2022-09-01 15:35:41 +08:00
pagedown
95f1a971b2 Docs: Mention clear_all_shortcuts in the FAQ about show_key kitten 2022-09-01 15:35:17 +08:00
Kovid Goyal
7968c88c67 ... 2022-09-01 07:54:36 +05:30
Kovid Goyal
992e90b0a3 Fix a regression in 0.26.0 that broke mapping of native keys who key codes did not fit in 21 bits
Fixes #5452
2022-08-31 22:19:51 +05:30
Kovid Goyal
afada84f52 Cleanup previous PR
Use wcswidth() for line width calculation. Works with emoji and ignores
escape codes automatically. Add changelog entry.
2022-08-31 08:01:52 +05:30
Kovid Goyal
b978ff8930 Merge branch 'fix/hints-next-line' of https://github.com/trygveaa/kitty 2022-08-31 07:56:38 +05:30
Kovid Goyal
053cede581 Use git rev-parse to get nightly commit hash 2022-08-31 07:22:56 +05:30
Trygve Aaberge
03720402a7 Fix hints sometimes matching next line as part of URL
For URLs where there's fewer characters left to the right edge of the
window than the number of escape characters in the line, the next line
would be included in the URL, as if the URL went all the way to the
right edge.

For example with a 40 chars wide terminal, if you run:

    echo -e '\e[31m1\e[m https://github.com/kovidgoyal/kitty\ntest'

And launch the hints kitten, you'll see that test on the next line will
be included in the URL.

This happened because the calculation for filling the rest of the line
with NUL characters counted the escape characters as well as the visible
characters, so it filled in too few characters.

This is a regression introduced in commit 91966712.
2022-08-30 22:24:58 +02:00
Kovid Goyal
fca0999814 Eureka! Figured out why libedit is breaking in prewarm on macOS via launchd
The prewarm zygote imports the world. shell.py had a top level import
for readline. Which means readline was being imported pre-fork. And of
course as is traditional with Apple libedit is not fork safe. Probably
because it initializes its internal IO routines based on the stdio
handles at time of import which are the handles kitty gets from launchd
2022-08-30 19:35:17 +05:30
Kovid Goyal
78056c659c Another place we cant use readline on macOS 2022-08-30 19:27:55 +05:30
Kovid Goyal
30e1b4680d Only override LANG in default env if it was not set either in the original os env or via the env config directive 2022-08-30 19:17:53 +05:30
Kovid Goyal
89a0c04d19 Fix exec in prewarm forked process sometimes inheriting env vars from grandparent process 2022-08-30 19:03:32 +05:30
Kovid Goyal
1ba027c277 Dont show LC_CTYPE in show_kitty_env_vars when running with system python on macOS 2022-08-30 16:55:17 +05:30
Kovid Goyal
d8e43a3412 macOS: Fix regression in 0.26.0 that caused asking the user for a line of input such as for set_tab_title to not work
Apparently libedit doesn't work in the forked process when the parent
process is run via Launch Services. I cant be bothered to investigate
why, given that libedit is closed source. macOS users will just have to
live without history/completion in the ask kitten until I get around to
writing a replacement for readline/libedit.

And on a personal note, macOS >> Necrotizing fasciitis

Fixes #5447
2022-08-30 16:45:14 +05:30
Kovid Goyal
644e46e91d Reconfigure all stdio streams in the prefork process 2022-08-30 16:22:59 +05:30
Kovid Goyal
e2a1f8dde7 ... 2022-08-30 10:40:59 +05:30
Kovid Goyal
7d09c6b762 Link to start of actual changelog 2022-08-30 10:27:53 +05:30
Kovid Goyal
6604e0d015 Fix regression in 0.26.0 that caused launching kitty without working STDIO handles to result in high CPU usage and prewarming failing
Fixes #5444
2022-08-30 08:15:13 +05:30
Kovid Goyal
bb2ec9cde7 ... 2022-08-30 07:50:36 +05:30
Kovid Goyal
89a2545855 Cleanup previous PR
Allow disabling hyperlinking of rg output. Also parse
--kitten=hyperlink=... form of CLI args.
2022-08-30 07:41:14 +05:30
Kovid Goyal
49f8c0eae6 Merge branch 'hyperlinked-grep-only-matches' of https://github.com/groves/kitty 2022-08-30 06:59:53 +05:30
Charlie Groves
db5b4da388 Add options for linking different parts of ripgrep output 2022-08-29 13:23:42 -04:00
Charlie Groves
8c7968a4d6 Describe --hyperlink-only-matches in the docs 2022-08-28 07:02:39 -04:00
Charlie Groves
f5a70adf0b Add a --hyperlink-only-matches flag to hyperlinked_grep
If it's active, only matching lines get links, not file headers or context lines.
2022-08-26 17:05:50 -04:00
44 changed files with 452 additions and 129 deletions

View File

@@ -35,6 +35,31 @@ mouse anywhere in the current command to move the cursor there. See
Detailed list of changes
-------------------------------------
0.26.2 [2022-09-05]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Allow creating :code:`overlay-main` windows, which are treated as the active window unlike normal overlays (:iss:`5392`)
- hints kitten: Allow using :doc:`launch` as the program to run, to open the result in a new kitty tab/window/etc. (:iss:`5462`)
- hyperlinked_grep kitten: Allow control over which parts of ``rg`` output are hyperlinked (:pull:`5428`)
- Fix regression in 0.26.0 that caused launching kitty without working STDIO handles to result in high CPU usage and prewarming failing (:iss:`5444`)
- :doc:`/launch`: Allow setting the margin and padding for newly created windows (:iss:`5463`)
- macOS: Fix regression in 0.26.0 that caused asking the user for a line of input such as for :ac:`set_tab_title` to not work (:iss:`5447`)
- hints kitten: hyperlink matching: Fix hints occasionally matching text on subsequent line as part of hyperlink (:pull:`5450`)
- Fix a regression in 0.26.0 that broke mapping of native keys whose key codes did not fit in 21 bits (:iss:`5452`)
- Wayland: Fix remembering window size not accurate when client side decorations are present
- Fix an issue where notification identifiers were not sanitized leading to
code execution if the user clicked on a notification popup from a malicious
source. Thanks to Carter Sande for discovering this vulnerability.
0.26.1 [2022-08-30]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -312,7 +312,11 @@ the :sc:`send_text <send_text>` you can use the ``show_key`` kitten. Run::
Then press the key you want to emulate. Note that this kitten will only show
keys that actually reach the terminal program, in particular, keys mapped to
actions in kitty will not be shown. To check those first map them to
:ac:`no_op`.
:ac:`no_op`. You can also start a kitty instance without any shortcuts to
interfere::
kitty -o clear_all_shortcuts=yes kitty +kitten show_key
How do I open a new window or tab with the same working directory as the current window?
--------------------------------------------------------------------------------------------

View File

@@ -32,7 +32,12 @@ Glossary
top of an existing kitty window, entirely covering it. Overlays are used
throughout kitty, for example, to display the :ref:`the scrollback buffer <scrollback>`,
to display :doc:`hints </kittens/hints>`, for :doc:`unicode input
</kittens/unicode_input>` etc.
</kittens/unicode_input>` etc. Normal overlays are meant for short
duration popups and so are not considered the :italic:`active window`
when determining the current working directory or getting input text for
kittens, launch commands, etc. To create an overlay considered as a
:italic:`main window` use the :code:`overlay-main` argument to
:doc:`launch`.
hyperlinks
Terminals can have hyperlinks, just like the internet. In kitty you can

View File

@@ -74,6 +74,16 @@ to :program:`rg`. How to do that varies based on the shell:
To learn more about kitty's powerful framework for customizing URL click
actions, see :doc:`here </open_actions>`.
By default, this kitten adds hyperlinks for several parts of ripgrep output:
the per-file header, match context lines, and match lines. You can control
which items are linked with a :command:`--kitten hyperlink` flag. For example,
:command:`--kitten hyperlink=matching_lines` will only add hyperlinks to the
match lines. :command:`--kitten hyperlink=file_headers,context_lines` will link
file headers and context lines but not match lines. :command:`--kitten
hyperlink=none` will cause the command line to be passed to directly to
:command:`rg` so no hyperlinking will be performed. :command:`--kitten
hyperlink` may be specified multiple times.
Hopefully, someday this functionality will make it into some `upstream grep
<https://github.com/BurntSushi/ripgrep/issues/665>`__ program directly removing
the need for this kitten.

2
glfw/wl_window.c vendored
View File

@@ -998,7 +998,7 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
if (window->decorated && !window->monitor && !window->wl.decorations.serverSide)
{
if (top)
*top = window->wl.decorations.metrics.top;
*top = window->wl.decorations.metrics.top - window->wl.decorations.metrics.visible_titlebar_height;
if (left)
*left = window->wl.decorations.metrics.width;
if (right)

View File

@@ -158,14 +158,17 @@ def extra_for(width: int, screen_width: int) -> int:
class Password(Handler):
def __init__(self, cli_opts: AskCLIOptions, prompt: str) -> None:
def __init__(self, cli_opts: AskCLIOptions, prompt: str, is_password: bool = True, initial_text: str = '') -> None:
self.cli_opts = cli_opts
self.prompt = prompt
self.initial_text = initial_text
from kittens.tui.line_edit import LineEdit
self.line_edit = LineEdit(is_password=True)
self.line_edit = LineEdit(is_password=is_password)
def initialize(self) -> None:
self.cmd.set_cursor_shape('beam')
if self.initial_text:
self.line_edit.on_text(self.initial_text, True)
self.draw_screen()
@Handler.atomic_update
@@ -205,7 +208,7 @@ class Password(Handler):
return ''
class Choose(Handler):
class Choose(Handler): # {{{
mouse_tracking = MouseTracking.buttons_only
def __init__(self, cli_opts: AskCLIOptions) -> None:
@@ -441,6 +444,7 @@ class Choose(Handler):
def on_interrupt(self) -> None:
self.quit_loop(1)
on_eot = on_interrupt
# }}}
def main(args: List[str]) -> Response:

View File

@@ -16,7 +16,7 @@ from typing import (
from kitty.cli import parse_args
from kitty.cli_stub import HintsCLIOptions
from kitty.constants import website_url
from kitty.fast_data_types import get_options, set_clipboard_string
from kitty.fast_data_types import get_options, set_clipboard_string, wcswidth
from kitty.key_encoding import KeyEvent
from kitty.typing import BossType, KittyCommonOpts
from kitty.utils import (
@@ -411,7 +411,10 @@ def convert_text(text: str, cols: int) -> str:
appended = False
for line in full_line.split('\r'):
if line:
lines.append(line.ljust(cols, '\0'))
line_sz = wcswidth(line)
if line_sz < cols:
line += '\0' * (cols - line_sz)
lines.append(line)
lines.append('\r')
appended = True
if appended:
@@ -536,12 +539,27 @@ OPTIONS = r'''
--program
type=list
What program to use to open matched text. Defaults to the default open program
for the operating system. Use a value of :code:`-` to paste the match into the
terminal window instead. A value of :code:`@` will copy the match to the
clipboard. A value of :code:`*` will copy the match to the primary selection
(on systems that support primary selections). A value of :code:`default` will
run the default open program. Can be specified multiple times to run multiple
programs.
for the operating system. Various special values are supported:
:code:`-`
paste the match into the terminal window.
:code:`@`
copy the match to the clipboard
:code:`*`
copy the match to the primary selection (on systems that support primary selections)
:code:`default`
run the default open program.
:code:`launch`
run :doc:`/launch` to open the program in a new kitty tab, window, overlay, etc.
For example::
--program "launch --type=tab vim"
Can be specified multiple times to run multiple programs.
--type
@@ -803,6 +821,7 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
elif program == '*':
set_primary_selection(joined_text())
else:
from kitty.conf.utils import to_cmdline
cwd = data['cwd']
program = get_options().open_url_with if program == 'default' else program
if text_type == 'hyperlink':
@@ -811,12 +830,20 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
if w is not None:
w.open_url(m, hyperlink_id=1, cwd=cwd)
else:
launch_args = []
if isinstance(program, str) and program.startswith('launch '):
launch_args = to_cmdline(program)
launch_args.insert(1, '--cwd=' + cwd)
for m, groupdict in zip(matches, groupdicts):
if groupdict:
m = []
for k, v in groupdict.items():
m.append('{}={}'.format(k, v or ''))
boss.open_url(m, program, cwd=cwd)
if launch_args:
w = boss.window_id_map.get(target_window_id)
boss.call_remote_control(active_window=w, args=tuple(launch_args + ([m] if isinstance(m, str) else m)))
else:
boss.open_url(m, program, cwd=cwd)
if __name__ == '__main__':

64
kittens/hyperlinked_grep/main.py Normal file → Executable file
View File

@@ -20,7 +20,49 @@ def write_hyperlink(write: Callable[[bytes], None], url: bytes, line: bytes, fra
def main() -> None:
if not sys.stdout.isatty() and '--pretty' not in sys.argv and '-p' not in sys.argv:
i = 1
all_link_options = {'matching_lines', 'context_lines', 'file_headers'}
link_options = set()
delegate_to_rg = False
def parse_link_options(raw: str) -> None:
nonlocal delegate_to_rg
if not raw:
raise SystemExit('Must specify an argument for --kitten option')
p, _, s = raw.partition('=')
if p != 'hyperlink':
raise SystemExit(f'Unknown argument for --kitten: {raw}')
for option in s.split(','):
if option == 'all':
link_options.update(all_link_options)
delegate_to_rg = False
elif option == 'none':
delegate_to_rg = True
link_options.clear()
elif option not in all_link_options:
a = ', '.join(sorted(all_link_options))
raise SystemExit(f"hyperlink option must be one of all, none, {a}, not '{option}'")
else:
link_options.add(option)
delegate_to_rg = False
while i < len(sys.argv):
if sys.argv[i] == '--kitten':
next_item = '' if i + 1 >= len(sys.argv) else sys.argv[i + 1]
parse_link_options(next_item)
del sys.argv[i:i+2]
elif sys.argv[i].startswith('--kitten='):
parse_link_options(sys.argv[i][len('--kitten='):])
del sys.argv[i]
else:
i += 1
if not link_options: # Default to linking everything if no options given
link_options.update(all_link_options)
link_file_headers = 'file_headers' in link_options
link_context_lines = 'context_lines' in link_options
link_matching_lines = 'matching_lines' in link_options
if delegate_to_rg or (not sys.stdout.isatty() and '--pretty' not in sys.argv and '-p' not in sys.argv):
os.execlp('rg', 'rg', *sys.argv[1:])
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
try:
@@ -31,7 +73,7 @@ def main() -> None:
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
sgr_pat = re.compile(br'\x1b\[.*?m')
osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\')
num_pat = re.compile(br'^(\d+)[:-]')
num_pat = re.compile(br'^(\d+)([:-])')
in_result: bytes = b''
hostname = socket.gethostname().encode('utf-8')
@@ -43,20 +85,22 @@ def main() -> None:
if not clean_line:
in_result = b''
write(b'\n')
continue
if in_result:
elif in_result:
m = num_pat.match(clean_line)
if m is not None:
write_hyperlink(write, in_result, line, frag=m.group(1))
else:
write(line)
is_match_line = m.group(2) == b':'
if (is_match_line and link_matching_lines) or (not is_match_line and link_context_lines):
write_hyperlink(write, in_result, line, frag=m.group(1))
continue
write(line)
else:
if line.strip():
path = quote_from_bytes(os.path.abspath(clean_line)).encode('utf-8')
in_result = b'file://' + hostname + path
write_hyperlink(write, in_result, line)
else:
write(line)
if link_file_headers:
write_hyperlink(write, in_result, line)
continue
write(line)
except KeyboardInterrupt:
p.send_signal(signal.SIGINT)
except (EOFError, BrokenPipeError):

View File

@@ -273,9 +273,9 @@ def save_as(conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFile
)
print('Relative paths will be resolved from:', styled(os.getcwd(), fg_intense=True, bold=True))
print()
from ..tui.path_completer import PathCompleter
from ..tui.path_completer import get_path
try:
dest = PathCompleter().input()
dest = get_path()
except (KeyboardInterrupt, EOFError):
return
if dest:

View File

@@ -121,11 +121,22 @@ class Handler:
self._tui_loop.quit(1)
def on_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None:
' Override this method and perform_default_key_action() to handle all key events '
if key_event.text:
self.on_text(key_event.text, in_bracketed_paste)
else:
self.on_key(key_event)
def perform_default_key_action(self, key_event: KeyEventType) -> bool:
' Override in sub-class if you want to handle these key events yourself '
if key_event.matches('ctrl+c'):
self.on_interrupt()
return True
if key_event.matches('ctrl+d'):
self.on_eot()
return True
return False
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
pass

View File

@@ -330,13 +330,8 @@ class Loop:
except Exception:
pass
else:
if k.matches('ctrl+c'):
self.handler.on_interrupt()
return
if k.matches('ctrl+d'):
self.handler.on_eot()
return
self.handler.on_key_event(k)
if not self.handler.perform_default_key_action(k):
self.handler.on_key_event(k)
def _on_pm(self, pm: str) -> None:
pass

View File

@@ -147,5 +147,9 @@ class PathCompleter:
return ''
def get_path(prompt: str = '> ') -> str:
return PathCompleter(prompt).input()
def develop() -> None:
PathCompleter().input()

View File

@@ -1702,7 +1702,10 @@ class Boss:
if actions:
self.drain_actions(actions)
if not found_action:
open_url(url, program or get_options().open_url_with, cwd=cwd)
extra_env = {}
if self.listening_on:
extra_env['KITTY_LISTEN_ON'] = self.listening_on
open_url(url, program or get_options().open_url_with, cwd=cwd, extra_env=extra_env)
@ac('misc', 'Click a URL using the keyboard')
def open_url_with_hints(self) -> None:
@@ -2414,8 +2417,11 @@ class Boss:
@ac('debug', 'Show the environment variables that the kitty process sees')
def show_kitty_env_vars(self) -> None:
w = self.active_window
env = os.environ.copy()
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not getattr(sys, 'kitty_run_data').get('lc_ctype_before_python'):
del env['LC_CTYPE']
if w:
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items())
output = '\n'.join(f'{k}={v}' for k, v in env.items())
self.display_scrollback(w, output, title=_('Current kitty env vars'), report_cursor=False)
@ac('debug', '''

View File

@@ -974,7 +974,7 @@ process_pending_resizes(monotonic_t now) {
static void
close_os_window(ChildMonitor *self, OSWindow *os_window) {
int w = os_window->window_width, h = os_window->window_height;
int w = os_window->content_area_width, h = os_window->content_area_height;
if (os_window->before_fullscreen.is_set && is_os_window_fullscreen(os_window)) {
w = os_window->before_fullscreen.w; h = os_window->before_fullscreen.h;
}

View File

@@ -181,8 +181,26 @@ spawn(PyObject *self UNUSED, PyObject *args) {
return PyLong_FromLong(pid);
}
#ifdef __APPLE__
#include <crt_externs.h>
#else
extern char **environ;
#endif
static PyObject*
clearenv_py(PyObject *self UNUSED, PyObject *args UNUSED) {
#ifdef __APPLE__
char **e = *_NSGetEnviron();
if (e) *e = NULL;
#else
if (environ) *environ = NULL;
#endif
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
METHODB(spawn, METH_VARARGS),
{"clearenv", clearenv_py, METH_NOARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@@ -169,7 +169,7 @@ def set_default_env(val: Optional[Dict[str, str]] = None) -> None:
def set_LANG_in_default_env(val: str) -> None:
default_env()['LANG'] = val
default_env().setdefault('LANG', val)
def openpty() -> Tuple[int, int]:

View File

@@ -187,6 +187,7 @@ def hostname() -> str:
def ref_hyperlink(x: str, prefix: str = '') -> str:
t, q = text_and_target(x)
url = f'kitty+doc://{hostname()}/#ref={prefix}{q}'
t = re.sub(r':([a-z]+):`([^`]+)`', r'\2', t)
return hyperlink_for_url(url, t)
@@ -293,7 +294,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
else:
prev_indent = 0
if prev_line:
current_cmd['help'] += '\n\n'
current_cmd['help'] += '\n' if current_cmd['help'].endswith('::') else '\n\n'
else:
state = NORMAL
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
@@ -429,6 +430,8 @@ class PrintHelpForSeq:
a('{}: {} {}{}'.format(title('Usage'), bold(yellow(appname)), optstring, usage))
a('')
message = message or default_msg
# replace rst literal code block syntax
message = message.replace('::\n\n', ':\n\n')
wa(prettify(message))
a('')
if seq:
@@ -448,6 +451,8 @@ class PrintHelpForSeq:
blocks[-1] += dt
if opt.get('help'):
t = help_text.replace('%default', str(defval)).strip()
# replace rst literal code block syntax
t = t.replace('::\n\n', ':\n\n')
t = t.replace('#placeholder_for_formatting#', '')
wa(prettify(t), indent=4)
if opt.get('choices'):
@@ -801,6 +806,7 @@ Output commands received from child process to STDOUT.
Replay previously dumped commands. Specify the path to a dump file previously
created by :option:`{appname} --dump-commands`. You
can open a new kitty window to replay the commands with::
{appname} sh -c "{appname} --replay-commands /path/to/dump/file; read"

View File

@@ -22,7 +22,7 @@ class Version(NamedTuple):
appname: str = 'kitty'
kitty_face = '🐱'
version: Version = Version(0, 26, 1)
version: Version = Version(0, 26, 2)
str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat

View File

@@ -1485,3 +1485,4 @@ class SingleKey:
def set_use_os_log(yes: bool) -> None: ...
def get_docs_ref_map() -> bytes: ...
def clearenv() -> None: ...

View File

@@ -97,6 +97,22 @@ min_size_for_os_window(OSWindow *window, int *min_width, int *min_height) {
}
static void
adjust_window_size_for_csd(OSWindow *w, int width, int height, int *adjusted_width, int *adjusted_height) {
*adjusted_width = width; *adjusted_height = height;
if (global_state.is_wayland) {
int left = -1, top, right, bottom;
glfwGetWindowFrameSize(w->handle, &left, &top, &right, &bottom);
if (left > -1) {
*adjusted_width -= left + right;
*adjusted_height -= top + bottom;
*adjusted_width = MAX(0, *adjusted_width);
*adjusted_height = MAX(0, *adjusted_height);
}
}
}
void
update_os_window_viewport(OSWindow *window, bool notify_boss) {
int w, h, fw, fh;
@@ -135,6 +151,7 @@ update_os_window_viewport(OSWindow *window, bool notify_boss) {
window->viewport_height = MAX(window->viewport_height, min_height);
window->window_width = MAX(w, min_width);
window->window_height = MAX(h, min_height);
adjust_window_size_for_csd(window, window->window_width, window->window_height, &window->content_area_width, &window->content_area_height);
if (notify_boss) {
call_boss(on_window_resize, "KiiO", window->id, window->viewport_width, window->viewport_height, dpi_changed ? Py_True : Py_False);
}
@@ -652,13 +669,14 @@ set_os_window_dpi(OSWindow *w) {
static bool
do_toggle_fullscreen(OSWindow *w, unsigned int flags, bool restore_sizes) {
int width, height, x, y;
int width, height, x, y, content_area_width, content_area_height;
glfwGetWindowSize(w->handle, &width, &height);
glfwGetWindowPos(w->handle, &x, &y);
adjust_window_size_for_csd(w, width, height, &content_area_width, &content_area_height);
bool was_maximized = glfwGetWindowAttrib(w->handle, GLFW_MAXIMIZED);
if (glfwToggleFullscreen(w->handle, flags)) {
w->before_fullscreen.is_set = true;
w->before_fullscreen.w = width; w->before_fullscreen.h = height; w->before_fullscreen.x = x; w->before_fullscreen.y = y;
w->before_fullscreen.w = content_area_width; w->before_fullscreen.h = content_area_height; w->before_fullscreen.x = x; w->before_fullscreen.y = y;
w->before_fullscreen.was_maximized = was_maximized;
return true;
}

View File

@@ -284,9 +284,9 @@ static PyMethodDef module_methods[] = {
};
// SingleKey {{{
typedef uint32_t keybitfield;
#define KEY_BITS 21
#define MOD_BITS 10
typedef uint64_t keybitfield;
#define KEY_BITS 51
#define MOD_BITS 12
#if 1 << (MOD_BITS-1) < GLFW_MOD_KITTY
#error "Not enough mod bits"
#endif
@@ -309,9 +309,9 @@ typedef struct {
} SingleKey;
static inline void
SingleKey_set_vals(SingleKey *self, long key, unsigned short mods, int is_native) {
if (key >= 0 && (unsigned long)key <= BIT_MASK(keybitfield, KEY_BITS)) {
keybitfield k = (keybitfield)(unsigned long)key;
SingleKey_set_vals(SingleKey *self, long long key, unsigned short mods, int is_native) {
if (key >= 0 && (unsigned long long)key <= BIT_MASK(keybitfield, KEY_BITS)) {
keybitfield k = (keybitfield)(unsigned long long)key;
self->key.key = k & BIT_MASK(keybitfield, KEY_BITS);
}
if (!(mods & 1 << (MOD_BITS + 1))) self->key.mods = mods & BIT_MASK(u_int32_t, MOD_BITS);
@@ -320,8 +320,8 @@ SingleKey_set_vals(SingleKey *self, long key, unsigned short mods, int is_native
static PyObject *
SingleKey_new(PyTypeObject *type, PyObject *args, PyObject *kw) {
long key = -1; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Hpl", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
long long key = -1; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|HpL", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
SingleKey *self = (SingleKey *)type->tp_alloc(type, 0);
if (self) SingleKey_set_vals(self, key, mods, is_native);
return (PyObject*)self;
@@ -341,8 +341,8 @@ SingleKey_repr(PyObject *s) {
unsigned int mods = self->key.mods;
if (mods) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "mods=%u, ", mods);
if (self->key.is_native) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "is_native=True, ");
unsigned long key = self->key.key;
if (key) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "key=%lu, ", key);
unsigned long long key = self->key.key;
if (key) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "key=%llu, ", key);
if (buf[pos-1] == ' ') pos -= 2;
pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, ")");
return PyUnicode_FromString(buf);
@@ -350,8 +350,8 @@ SingleKey_repr(PyObject *s) {
static PyObject*
SingleKey_get_key(SingleKey *self, void UNUSED *closure) {
const unsigned long val = self->key.key;
return PyLong_FromUnsignedLong(val);
const unsigned long long val = self->key.key;
return PyLong_FromUnsignedLongLong(val);
}
static PyObject*
@@ -436,8 +436,8 @@ SingleKey_resolve_kitty_mod(SingleKey *self, PyObject *km) {
static PyObject*
SingleKey_replace(SingleKey *self, PyObject *args, PyObject *kw) {
long key = -2; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Hpl", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
long long key = -2; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|HpL", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
SingleKey *ans = (SingleKey*)SingleKey_Type.tp_alloc(&SingleKey_Type, 0);
if (ans) {
if (key == -1) key = 0;

View File

@@ -21,7 +21,7 @@ from .fast_data_types import (
)
from .options.utils import env as parse_env
from .tabs import Tab, TabManager
from .types import run_once
from .types import OverlayType, run_once
from .utils import (
get_editor, log_error, resolve_custom_file, set_primary_selection, which
)
@@ -56,7 +56,7 @@ of the active window in the tab is used as the tab title. The special value
--type
type=choices
default=window
choices=window,tab,os-window,overlay,background,clipboard,primary
choices=window,tab,os-window,overlay,overlay-main,background,clipboard,primary
Where to launch the child process:
:code:`window`
@@ -71,6 +71,13 @@ Where to launch the child process:
:code:`overlay`
An :term:`overlay window <overlay>` covering the current active kitty window
:code:`overlay-main`
An :term:`overlay window <overlay>` covering the current active kitty window.
Unlike a plain overlay window, this window is considered as a :italic:`main`
window which means it is used as the active window for getting the current working
directory, the input text for kittens, launch commands, etc. Useful if this overlay is
intended to run for a long time as a primary window.
:code:`background`
The process will be run in the :italic:`background`, without a kitty window.
@@ -165,12 +172,14 @@ type=list
Restrict the actions remote control is allowed to take. This works like
:opt:`remote_control_password`. You can specify a password and list of actions
just as for :opt:`remote_control_password`. For example::
--remote-control-password '"my passphrase" get-* set-colors'
This password will be in effect for this window only.
Note that any passwords you have defined for :opt:`remote_control_password`
in :file:`kitty.conf` are also in effect. You can override them by using the same password here.
You can also disable all :opt:`remote_control_password` global passwords for this window, by using::
--remote-control-password '!'
This option only takes effect if :option:`--allow-remote-control`
@@ -271,8 +280,17 @@ effect if :option:`--logo` is specified. See :opt:`window_logo_alpha`.
type=list
Change colors in the newly launched window. You can either specify a path to a
:file:`.conf` file with the same syntax as :file:`kitty.conf` to read the colors
from, or specify them individually, for example: :code:`--color background=white
--color foreground=red`.
from, or specify them individually, for example::
--color background=white --color foreground=red
--spacing
type=list
Set the margin and padding for the newly created window.
For example: :code:`margin=20` or :code:`padding-left=10` or :code:`margin-h=30`. The shorthand form sets
all values, the :code:`*-h` and :code:`*-v` variants set horizontal and vertical values.
Can be specified multiple times.
--watcher -w
@@ -454,6 +472,10 @@ def launch(
'overlay_for': None,
'stdin': None
}
spacing = {}
if opts.spacing:
from .rc.set_spacing import parse_spacing_settings, patch_window_edges
spacing = parse_spacing_settings(opts.spacing)
if opts.cwd:
if opts.cwd == 'current':
if active:
@@ -524,7 +546,7 @@ def launch(
kw['cmd'] = final_cmd
if force_window_launch and opts.type not in non_window_launch_types:
opts.type = 'window'
if opts.type == 'overlay' and active:
if opts.type in ('overlay', 'overlay-main') and active:
kw['overlay_for'] = active.id
if opts.type == 'background':
cmd = kw['cmd']
@@ -549,12 +571,17 @@ def launch(
if tab is not None:
watchers = load_watch_modules(opts.watcher)
new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, **kw)
if spacing:
patch_window_edges(new_window, spacing)
tab.relayout()
if opts.color:
apply_colors(new_window, opts.color)
if opts.keep_focus and active:
boss.set_active_window(active, switch_os_window_if_needed=True, for_keep_focus=True)
if opts.logo:
new_window.set_logo(opts.logo, opts.logo_position or '', opts.logo_alpha)
if opts.type == 'overlay-main':
new_window.overlay_type = OverlayType.main
return new_window
return None
@@ -564,7 +591,7 @@ def clone_safe_opts() -> FrozenSet[str]:
return frozenset((
'window_title', 'tab_title', 'type', 'keep_focus', 'cwd', 'env', 'hold',
'location', 'os_window_class', 'os_window_name', 'os_window_title',
'logo', 'logo_position', 'logo_alpha', 'color'
'logo', 'logo_position', 'logo_alpha', 'color', 'spacing',
))

View File

@@ -17,6 +17,7 @@
#include <stdint.h>
#include <wchar.h>
#include <Python.h>
#include <fcntl.h>
#ifndef KITTY_LIB_PATH
#define KITTY_LIB_PATH "../.."
@@ -268,9 +269,55 @@ read_exe_path(char *exe, size_t buf_sz) {
}
#endif // }}}
static bool
is_valid_fd(int fd)
{
// This is copied from the python source code as we need the exact same semantics
// to prevent python from giving us None for sys.stdout and friends.
#if defined(F_GETFD) && ( \
defined(__linux__) || \
defined(__APPLE__) || \
defined(__wasm__))
return fcntl(fd, F_GETFD) >= 0;
#elif defined(__linux__)
int fd2 = dup(fd);
if (fd2 >= 0) {
close(fd2);
}
return (fd2 >= 0);
#else
struct stat st;
return (fstat(fd, &st) == 0);
#endif
}
static bool
reopen_to_null(const char *mode, FILE *stream) {
errno = 0;
while (true) {
if (freopen("/dev/null", mode, stream) != NULL) return true;
if (errno == EINTR) continue;
perror("Failed to re-open STDIO handle to /dev/null");
return false;
}
}
static bool
ensure_working_stdio(void) {
#define C(which, mode) { \
int fd = fileno(which); \
if (fd < 0) { if (!reopen_to_null(mode, which)) return false; } \
else if (!is_valid_fd(fd)) { \
close(fd); if (!reopen_to_null(mode, which)) return false; \
}}
C(stdin, "r") C(stdout, "w") C(stderr, "w")
return true;
#undef C
}
int main(int argc, char *argv[], char* envp[]) {
if (argc < 1 || !argv) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
if (!ensure_working_stdio()) return 1;
char exe[PATH_MAX+1] = {0};
char exe_dir_buf[PATH_MAX+1] = {0};
FREE_AFTER_FUNCTION const char *lc_ctype = NULL;

View File

@@ -379,6 +379,7 @@ def _main() -> None:
if is_macos and os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES', None) == '1':
os.chdir(os.path.expanduser('~'))
args = macos_cmdline(args)
getattr(sys, 'kitty_run_data')['launched_by_launch_services'] = True
try:
cwd_ok = os.path.isdir(os.getcwd())
except Exception:

View File

@@ -1,13 +1,15 @@
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
import re
from base64 import standard_b64decode
from collections import OrderedDict
from itertools import count
from typing import Dict, Optional, Callable
from typing import Callable, Dict, Optional
from .constants import is_macos, logo_png_file
from .fast_data_types import get_boss
from .types import run_once
from .utils import log_error
NotifyImplementation = Callable[[str, str, str], None]
@@ -92,6 +94,11 @@ def parse_osc_777(raw: str) -> NotificationCommand:
return ans
@run_once
def sanitize_identifier_pat() -> 're.Pattern[str]':
return re.compile(r'[^a-zA-Z0-9-_+.]+')
def parse_osc_99(raw: str) -> NotificationCommand:
cmd = NotificationCommand()
metadata, payload = raw.partition(';')[::2]
@@ -107,7 +114,7 @@ def parse_osc_99(raw: str) -> NotificationCommand:
if k == 'p':
payload_type = v
elif k == 'i':
cmd.identifier = v
cmd.identifier = sanitize_identifier_pat().sub('', v)
elif k == 'e':
payload_is_encoded = v == '1'
elif k == 'd':

View File

@@ -24,9 +24,9 @@ from typing import (
from kitty.constants import kitty_exe, running_in_kitty
from kitty.entry_points import main as main_entry_point
from kitty.fast_data_types import (
CLD_EXITED, CLD_KILLED, CLD_STOPPED, get_options, install_signal_handlers,
read_signals, remove_signal_handlers, safe_pipe, set_options,
set_use_os_log
CLD_EXITED, CLD_KILLED, CLD_STOPPED, clearenv, get_options,
install_signal_handlers, read_signals, remove_signal_handlers, safe_pipe,
set_options, set_use_os_log
)
from kitty.options.types import Options
from kitty.shm import SharedMemory
@@ -293,6 +293,13 @@ def child_main(cmd: Dict[str, Any], ready_fd: int = -1, prewarm_type: str = 'dir
env = cmd.get('env')
if env is not None:
os.environ.clear()
# os.environ.clear() does not delete all existing env vars from the
# libc environ pointer in some circumstances, I havent figured out
# which exactly. Presumably there is something that alters the
# libc environ pointer?? The environ pointer is used by os.exec and
# therefore by subprocess and friends, so we need to ensure it is
# cleared.
clearenv()
os.environ.update(env)
argv = cmd.get('argv')
if argv:
@@ -567,8 +574,9 @@ def exec_main(stdin_read: int, stdout_write: int, death_notify_write: int) -> No
os.set_inheritable(stdout_write, False)
os.set_inheritable(death_notify_write, False)
running_in_kitty(False)
if not sys.stdout.line_buffering: # happens if the parent kitty instance has stdout not pointing to a terminal
sys.stdout.reconfigure(line_buffering=True) # type: ignore
for x in (sys.stdout, sys.stdin, sys.stderr):
if not x.line_buffering: # happens if the parent kitty instance has stdout not pointing to a terminal
x.reconfigure(line_buffering=True) # type: ignore
try:
main(stdin_read, stdout_write, death_notify_write)
finally:

View File

@@ -41,6 +41,7 @@ class Launch(RemoteCommand):
@first_cmd_output_on_screen.@last_cmd_output.@last_visited_cmd_output: Where to get stdin for the process from
stdin_add_formatting/bool: Boolean indicating whether to add formatting codes to stdin
stdin_add_line_wrap_markers/bool: Boolean indicating whether to add line wrap markers to stdin
spacing/list.str: A list of spacing specifications, see the docs for the set-spacing command
no_response/bool: Boolean indicating whether to send back the window id
marker/str: Specification for marker for new window, for example: "text 1 ERROR"
logo/str: Path to window logo
@@ -53,8 +54,8 @@ class Launch(RemoteCommand):
desc = (
'Prints out the id of the newly opened window. Any command line arguments'
' are assumed to be the command line used to run in the new window, if none'
' are provided, the default shell is run. For example:\n'
':code:`kitty @ launch --title=Email mutt`'
' are provided, the default shell is run. For example::\n\n'
' kitty @ launch --title=Email mutt'
)
options_spec = MATCH_TAB_OPTION + '\n\n' + '''\
--no-response

View File

@@ -34,8 +34,8 @@ class NewWindow(RemoteCommand):
' Prints out the id of the newly opened window'
' (unless :option:`--no-response` is used). Any command line arguments'
' are assumed to be the command line used to run in the new window, if none'
' are provided, the default shell is run. For example:\n'
':code:`kitty @ new-window --title Email mutt`'
' are provided, the default shell is run. For example::\n\n'
' kitty @ new-window --title Email mutt'
)
options_spec = MATCH_TAB_OPTION + '''\n
--title

View File

@@ -27,7 +27,8 @@ class SetBackgroundOpacity(RemoteCommand):
desc = (
'Set the background opacity for the specified windows. This will only work if you have turned on'
' :opt:`dynamic_background_opacity` in :file:`kitty.conf`. The background opacity affects all kitty windows in a'
' single OS window. For example: :code:`kitty @ set-background-opacity 0.5`'
' single OS window. For example::\n\n'
' kitty @ set-background-opacity 0.5'
)
options_spec = '''\
--all -a

View File

@@ -68,7 +68,8 @@ class SetColors(RemoteCommand):
'Set the terminal colors for the specified windows/tabs (defaults to active window).'
' You can either specify the path to a conf file'
' (in the same format as :file:`kitty.conf`) to read the colors from or you can specify individual colors,'
' for example: :code:`kitty @ set-colors foreground=red background=white`'
' for example::\n\n'
' kitty @ set-colors foreground=red background=white'
)
options_spec = '''\
--all -a

View File

@@ -2,7 +2,7 @@
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING, Dict, Optional, List
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType,
@@ -37,6 +37,34 @@ def patch_configured_edges(opts: 'Options', s: Dict[str, Optional[float]]) -> No
setattr(opts, q, new_edges)
def parse_spacing_settings(args: Iterable[str]) -> Dict[str, Optional[float]]:
mapper: Dict[str, List[str]] = {}
for q in ('margin', 'padding'):
mapper[q] = f'{q}-left {q}-top {q}-right {q}-bottom'.split()
mapper[f'{q}-h'] = mapper[f'{q}-horizontal'] = f'{q}-left {q}-right'.split()
mapper[f'{q}-v'] = mapper[f'{q}-vertical'] = f'{q}-top {q}-bottom'.split()
for edge in ('left', 'top', 'right', 'bottom'):
mapper[f'{q}-{edge}'] = [f'{q}-{edge}']
settings: Dict[str, Optional[float]] = {}
for spec in args:
parts = spec.split('=', 1)
if len(parts) != 2:
raise ValueError(f'{spec} is not a valid setting')
which = mapper.get(parts[0].lower())
if not which:
raise ValueError(f'{parts[0]} is not a valid edge specification')
if parts[1].lower() == 'default':
val = None
else:
try:
val = float(parts[1])
except Exception:
raise ValueError(f'{parts[1]} is not a number')
for q in which:
settings[q] = val
return settings
class SetSpacing(RemoteCommand):
'''
@@ -70,32 +98,12 @@ windows).
argspec = 'MARGIN_OR_PADDING ...'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
settings: Dict[str, Optional[float]] = {}
mapper: Dict[str, List[str]] = {}
for q in ('margin', 'padding'):
mapper[q] = f'{q}-left {q}-top {q}-right {q}-bottom'.split()
mapper[f'{q}-h'] = mapper[f'{q}-horizontal'] = f'{q}-left {q}-right'.split()
mapper[f'{q}-v'] = mapper[f'{q}-vertical'] = f'{q}-top {q}-bottom'.split()
for edge in ('left', 'top', 'right', 'bottom'):
mapper[f'{q}-{edge}'] = [f'{q}-{edge}']
if not args:
self.fatal('At least one setting must be specified')
for spec in args:
parts = spec.split('=', 1)
if len(parts) != 2:
self.fatal(f'{spec} is not a valid setting')
which = mapper.get(parts[0].lower())
if not which:
self.fatal(f'{parts[0]} is not a valid edge specification')
if parts[1].lower() == 'default':
val = None
else:
try:
val = float(parts[1])
except Exception:
self.fatal(f'{parts[1]} is not a number')
for q in which:
settings[q] = val
try:
settings = parse_spacing_settings(args)
except Exception as e:
self.fatal(str(e))
ans = {
'match_window': opts.match, 'match_tab': opts.match_tab,
'all': opts.all, 'configured': opts.configured,

View File

@@ -2,7 +2,6 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import readline
import shlex
import sys
import traceback
@@ -39,6 +38,7 @@ def match_commands() -> Tuple[str, ...]:
@run_once
def init_readline() -> None:
import readline
global is_libedit
with suppress(OSError):
readline.read_init_file()
@@ -90,6 +90,7 @@ class Completer:
self.history_path = os.path.join(ddir, 'shell.history')
def complete(self, text: str, state: int) -> Optional[str]:
import readline
if state == 0:
line = readline.get_line_buffer()
cmdline = shlex.split(line)
@@ -102,6 +103,7 @@ class Completer:
return None
def __enter__(self) -> 'Completer':
import readline
with suppress(Exception):
readline.read_history_file(self.history_path)
readline.set_completer(self.complete)
@@ -110,6 +112,7 @@ class Completer:
return self
def __exit__(self, *a: Any) -> None:
import readline
readline.write_history_file(self.history_path)

View File

@@ -188,7 +188,7 @@ typedef struct {
int x, y, w, h;
bool is_set, was_maximized;
} before_fullscreen;
int viewport_width, viewport_height, window_width, window_height;
int viewport_width, viewport_height, window_width, window_height, content_area_width, content_area_height;
double viewport_x_ratio, viewport_y_ratio;
Tab *tabs;
BackgroundImage *bgimage;

View File

@@ -211,7 +211,7 @@ class Tab: # {{{
@property
def active_window_for_cwd(self) -> Optional[Window]:
return self.windows.active_group_base
return self.windows.active_group_main
@property
def title(self) -> str:

View File

@@ -1,16 +1,24 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
from enum import Enum
from functools import update_wrapper
from typing import (
TYPE_CHECKING, Any, Callable, Generic, NamedTuple, Tuple, TypeVar, Union, Iterator, Dict
TYPE_CHECKING, Any, Callable, Dict, Generic, Iterator, NamedTuple, Tuple,
TypeVar, Union
)
if TYPE_CHECKING:
from kitty.fast_data_types import SingleKey
_T = TypeVar('_T')
class OverlayType(Enum):
transient: str = 'transient'
main: str = 'main'
class ParsedShortcut(NamedTuple):
mods: int
key_name: str
@@ -160,11 +168,11 @@ def run_once(f: Callable[[], _T]) -> 'RunOnce[_T]':
@run_once
def modmap() -> Dict[str, int]:
from .constants import is_macos
from .fast_data_types import (
GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER,
GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER
)
from .constants import is_macos
return {'ctrl': GLFW_MOD_CONTROL, 'shift': GLFW_MOD_SHIFT, ('opt' if is_macos else 'alt'): GLFW_MOD_ALT,
('cmd' if is_macos else 'super'): GLFW_MOD_SUPER, 'hyper': GLFW_MOD_HYPER, 'meta': GLFW_MOD_META,

View File

@@ -269,7 +269,8 @@ def command_for_open(program: Union[str, List[str]] = 'default') -> List[str]:
return cmd
def open_cmd(cmd: Union[Iterable[str], List[str]], arg: Union[None, Iterable[str], str] = None, cwd: Optional[str] = None) -> 'PopenType[bytes]':
def open_cmd(cmd: Union[Iterable[str], List[str]], arg: Union[None, Iterable[str], str] = None,
cwd: Optional[str] = None, extra_env: Optional[Dict[str, str]] = None) -> 'PopenType[bytes]':
import subprocess
if arg is not None:
cmd = list(cmd)
@@ -277,13 +278,17 @@ def open_cmd(cmd: Union[Iterable[str], List[str]], arg: Union[None, Iterable[str
cmd.append(arg)
else:
cmd.extend(arg)
env: Optional[Dict[str, str]] = None
if extra_env:
env = os.environ.copy()
env.update(extra_env)
return subprocess.Popen(
tuple(cmd), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd or None,
preexec_fn=clear_handled_signals)
preexec_fn=clear_handled_signals, env=env)
def open_url(url: str, program: Union[str, List[str]] = 'default', cwd: Optional[str] = None) -> 'PopenType[bytes]':
return open_cmd(command_for_open(program), url, cwd=cwd)
def open_url(url: str, program: Union[str, List[str]] = 'default', cwd: Optional[str] = None, extra_env: Optional[Dict[str, str]] = None) -> 'PopenType[bytes]':
return open_cmd(command_for_open(program), url, cwd=cwd, extra_env=extra_env)
def detach(fork: bool = True, setsid: bool = True, redirect: bool = True) -> None:

View File

@@ -41,11 +41,11 @@ from .fast_data_types import (
update_window_title, update_window_visibility, wakeup_main_loop
)
from .keys import keyboard_mode_name, mod_mask
from .notify import NotificationCommand, handle_notification_cmd
from .notify import NotificationCommand, handle_notification_cmd, sanitize_identifier_pat
from .options.types import Options
from .rgb import to_color
from .terminfo import get_capabilities
from .types import MouseEvent, WindowGeometry, ac, run_once
from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once
from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
from .utils import (
docs_url, get_primary_selection, kitty_ansi_sanitizer_pat, load_shaders,
@@ -462,6 +462,7 @@ global_watchers = GlobalWatchers()
class Window:
window_custom_type: str = ''
overlay_type = OverlayType.transient
def __init__(
self,
@@ -642,6 +643,8 @@ class Window:
}
if self.window_custom_type:
ans['window_custom_type'] = self.window_custom_type
if self.overlay_type is not OverlayType.transient:
ans['overlay_type'] = self.overlay_type.value
return ans
@property
@@ -998,6 +1001,7 @@ class Window:
self.screen.send_escape_code_to_child(OSC, f'{code};rgb:{r:04x}/{g:04x}/{b:04x}')
def report_notification_activated(self, identifier: str) -> None:
identifier = sanitize_identifier_pat().sub('', identifier)
self.screen.send_escape_code_to_child(OSC, f'99;i={identifier};')
def set_dynamic_color(self, code: int, value: Union[str, bytes]) -> None:

View File

@@ -7,7 +7,7 @@ from contextlib import suppress
from itertools import count
from typing import Any, Deque, Dict, Iterator, List, Optional, Tuple, Union
from .types import WindowGeometry
from .types import OverlayType, WindowGeometry
from .typing import EdgeLiteral, TabType, WindowType
WindowOrId = Union[WindowType, int]
@@ -54,7 +54,10 @@ class WindowGroup:
return False
@property
def base_window_id(self) -> int:
def main_window_id(self) -> int:
for w in reversed(self.windows):
if w.overlay_type is OverlayType.main:
return w.id
return self.windows[0].id if self.windows else 0
@property
@@ -298,9 +301,9 @@ class WindowList:
return None
@property
def active_group_base(self) -> Optional[WindowType]:
def active_group_main(self) -> Optional[WindowType]:
with suppress(Exception):
return self.id_map[self.groups[self.active_group_idx].base_window_id]
return self.id_map[self.groups[self.active_group_idx].main_window_id]
return None
def set_active_window_group_for(self, x: WindowOrId, for_keep_focus: Optional[WindowType] = None) -> None:

View File

@@ -107,6 +107,22 @@ class TestBuild(BaseTest):
run_tests(partial(docs_url, local_docs_root=None), w, '/')
self.ae(docs_url('#ref=issues-123'), 'https://github.com/kovidgoyal/kitty/issues/123')
def test_launcher_ensures_stdio(self):
from kitty.constants import kitty_exe
import subprocess
exe = kitty_exe()
cp = subprocess.run([exe, '+runpy', f'''\
import os, sys
if sys.stdin:
os.close(sys.stdin.fileno())
if sys.stdout:
os.close(sys.stdout.fileno())
if sys.stderr:
os.close(sys.stderr.fileno())
os.execlp({exe!r}, 'kitty', '+runpy', 'import sys; raise SystemExit(1 if sys.stdout is None or sys.stdin is None or sys.stderr is None else 0)')
'''])
self.assertEqual(cp.returncode, 0)
def main() -> None:
tests = unittest.defaultTestLoader.loadTestsFromTestCase(TestBuild)

View File

@@ -535,7 +535,7 @@ class TestDataTypes(BaseTest):
q('a\x1b[bc', 'ac')
q('a\x1b[12;34:43mbc', 'abc')
def test_SingleKey(self):
def test_single_key(self):
from kitty.fast_data_types import (
GLFW_MOD_KITTY, GLFW_MOD_SHIFT, SingleKey
)
@@ -548,9 +548,14 @@ class TestDataTypes(BaseTest):
self.ae(repr(SingleKey(key=23, mods=2, is_native=True)), 'SingleKey(mods=2, is_native=True, key=23)')
self.ae(repr(SingleKey(key=23, mods=2)), 'SingleKey(mods=2, key=23)')
self.ae(repr(SingleKey(key=23)), 'SingleKey(key=23)')
self.ae(repr(SingleKey(key=0x1008ff57)), 'SingleKey(key=269025111)')
self.ae(repr(SingleKey(key=23)._replace(mods=2)), 'SingleKey(mods=2, key=23)')
self.ae(repr(SingleKey(key=23)._replace(key=-1, mods=GLFW_MOD_KITTY)), f'SingleKey(mods={GLFW_MOD_KITTY})')
self.assertEqual(SingleKey(key=1), SingleKey(key=1))
self.assertEqual(hash(SingleKey(key=1)), hash(SingleKey(key=1)))
self.assertNotEqual(hash(SingleKey(key=1, mods=2)), hash(SingleKey(key=1)))
self.assertNotEqual(SingleKey(key=1, mods=2), SingleKey(key=1))
def test_notify_identifier_sanitization(self):
from kitty.notify import sanitize_identifier_pat
self.ae(sanitize_identifier_pat().sub('', '\x1b\nabc\n[*'), 'abc')

View File

@@ -10,13 +10,14 @@ class TestHints(BaseTest):
def test_url_hints(self):
from kittens.hints.main import (
Mark, convert_text, functions_for, linenum_marks,
linenum_process_result, mark, parse_hints_args
linenum_process_result, mark, parse_hints_args, process_escape_codes
)
args = parse_hints_args([])[0]
pattern, post_processors = functions_for(args)
def create_marks(text, cols=20, mark=mark):
text = convert_text(text, cols)
text, _ = process_escape_codes(text)
return tuple(mark(pattern, post_processors, text, args))
def t(text, url, cols=20):
@@ -32,6 +33,8 @@ class TestHints(BaseTest):
t(f'link:{u}[xxx]', u)
t(f'`xyz <{u}>`_.', u)
t(f'<a href="{u}">moo', u)
t('\x1b[mhttp://test.me/1234\n\x1b[mx', 'http://test.me/1234')
t('\x1b[mhttp://test.me/12345\r\x1b[m6\n\x1b[mx', 'http://test.me/123456')
def m(text, path, line, cols=20):

View File

@@ -28,13 +28,15 @@ class Prewarm(BaseTest):
cwd = tempfile.gettempdir()
env = {'TEST_ENV_PASS': 'xyz'}
cols = 117
cols = 317
stdin_data = 'from_stdin'
pty = self.create_pty(cols=cols)
ttyname = os.ttyname(pty.slave_fd)
opts = get_options()
opts.config_overrides = 'font_family prewarm',
os.environ['SHOULD_NOT_BE_PRESENT'] = '1'
p = fork_prewarm_process(opts, use_exec=True)
del os.environ['SHOULD_NOT_BE_PRESENT']
if p is None:
return
p.take_from_worker_fd(create_file=True)
@@ -44,7 +46,7 @@ import os, json; from kitty.utils import *; from kitty.fast_data_types import ge
'ttyname': os.ttyname(sys.stdout.fileno()),
'cols': read_screen_size().cols,
'cwd': os.getcwd(),
'env': os.environ.get('TEST_ENV_PASS'),
'env': os.environ.copy(),
'pid': os.getpid(),
'font_family': get_options().font_family,
'stdin': sys.stdin.read(),
@@ -59,7 +61,8 @@ import os, json; from kitty.utils import *; from kitty.fast_data_types import ge
self.assertTrue(data['cterm'])
self.ae(data['ttyname'], ttyname)
self.ae(os.path.realpath(data['cwd']), os.path.realpath(cwd))
self.ae(data['env'], env['TEST_ENV_PASS'])
self.ae(data['env']['TEST_ENV_PASS'], env['TEST_ENV_PASS'])
self.assertNotIn('SHOULD_NOT_BE_PRESENT', data['env'])
self.ae(data['font_family'], 'prewarm')
self.ae(int(p.from_worker.readline()), data['pid'])

View File

@@ -368,4 +368,4 @@ PS1="{ps1}"
pty.send_cmd_to_child('clone-in-kitty')
pty.wait_till(lambda: len(pty.callbacks.clone_cmds) == 1)
env = pty.callbacks.clone_cmds[0].env
self.ae(env.get('ES'), 'a\n `b` c\n$d')
self.ae(env.get('ES'), 'a\n `b` c\n$d', f'Screen contents: {pty.screen_contents()!r}')

View File

@@ -269,13 +269,7 @@ class GitHub(Base): # {{{
def update_nightly_description(self, release_id: int) -> None:
url = f'{self.url_base}/{release_id}'
now = str(datetime.datetime.utcnow()).split('.')[0] + ' UTC'
try:
with open('.git/refs/heads/master') as f:
commit = f.read().strip()
except FileNotFoundError:
time.sleep(1)
with open('.git/refs/heads/master') as f:
commit = f.read().strip()
commit = subprocess.check_output(['git', 'rev-parse', '--verify', '--end-of-options', 'master^{commit}']).decode('utf-8').strip()
self.patch(
url, 'Failed to update nightly release description',
body=f'Nightly release, generated on: {now} from commit: {commit}.'
@@ -368,7 +362,7 @@ class GitHub(Base): # {{{
'target_commitish': 'master',
'name': f'version {self.version}',
'body': f'Release version {self.version}.'
' For changelog, see https://sw.kovidgoyal.net/kitty/changelog/'
' For changelog, see https://sw.kovidgoyal.net/kitty/changelog/#detailed-list-of-changes'
' GPG key used for signing tarballs is: https://calibre-ebook.com/signatures/kovid.gpg',
'draft': False,
'prerelease': False