Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bdf27fd78 | ||
|
|
f05783e64d | ||
|
|
c455fea729 | ||
|
|
5bf5454dbe | ||
|
|
b60d9f73ce | ||
|
|
31fe35cd38 | ||
|
|
607bc845fc | ||
|
|
a420d9d19c | ||
|
|
4b81e4936e | ||
|
|
295743a96e | ||
|
|
d3a1848c27 | ||
|
|
9cec924f32 | ||
|
|
67c963ab89 | ||
|
|
0f7ced25c1 | ||
|
|
42948ffff0 | ||
|
|
922ce5bdea | ||
|
|
1b42486026 | ||
|
|
95f1a971b2 | ||
|
|
7968c88c67 | ||
|
|
992e90b0a3 | ||
|
|
afada84f52 | ||
|
|
b978ff8930 | ||
|
|
053cede581 | ||
|
|
03720402a7 | ||
|
|
fca0999814 | ||
|
|
78056c659c | ||
|
|
30e1b4680d | ||
|
|
89a0c04d19 | ||
|
|
1ba027c277 | ||
|
|
d8e43a3412 | ||
|
|
644e46e91d | ||
|
|
e2a1f8dde7 | ||
|
|
7d09c6b762 | ||
|
|
6604e0d015 | ||
|
|
bb2ec9cde7 | ||
|
|
89a2545855 | ||
|
|
49f8c0eae6 | ||
|
|
db5b4da388 | ||
|
|
8c7968a4d6 | ||
|
|
f5a70adf0b |
@@ -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]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -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?
|
||||
--------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
2
glfw/wl_window.c
vendored
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
64
kittens/hyperlinked_grep/main.py
Normal file → Executable 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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -147,5 +147,9 @@ class PathCompleter:
|
||||
return ''
|
||||
|
||||
|
||||
def get_path(prompt: str = '> ') -> str:
|
||||
return PathCompleter(prompt).input()
|
||||
|
||||
|
||||
def develop() -> None:
|
||||
PathCompleter().input()
|
||||
|
||||
@@ -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', '''
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
};
|
||||
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1485,3 +1485,4 @@ class SingleKey:
|
||||
|
||||
def set_use_os_log(yes: bool) -> None: ...
|
||||
def get_docs_ref_map() -> bytes: ...
|
||||
def clearenv() -> None: ...
|
||||
|
||||
22
kitty/glfw.c
22
kitty/glfw.c
@@ -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;
|
||||
}
|
||||
|
||||
28
kitty/keys.c
28
kitty/keys.c
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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}')
|
||||
|
||||
10
publish.py
10
publish.py
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user