feat(kitty): added scroll helper to kitty

This commit is contained in:
Sergio Laín 2023-12-15 01:10:21 +01:00
parent f39b276753
commit 5e465242e7
No known key found for this signature in database
GPG key ID: 14C9B8080681777B
4 changed files with 362 additions and 0 deletions

View file

@ -0,0 +1,3 @@
#!/bin/sh
[ "$1" = @selection ] && set -- .
xdg-open "$1"

View file

@ -1699,6 +1699,9 @@ map cmd+enter new_window
#:: map kitty_mod+n new_os_window
map cmd+n launch --type os-window --cwd=current
#:: Kitty search
map kitty_mod+/ launch --location=hsplit --allow-remote-control kitty +kitten search.py @active-kitty-window-id
#:: Works like new_window above, except that it opens a top-level OS
#:: window. In particular you can use new_os_window_with_cwd to open
#:: a window with the current working directory.

View file

@ -0,0 +1,18 @@
from kittens.tui.handler import result_handler
from kitty.boss import Boss
def main(args: list[str]) -> None:
pass
@result_handler(no_ui=True)
def handle_result(
args: list[str], answer: str, target_window_id: int, boss: Boss
) -> None:
w = boss.window_id_map.get(target_window_id)
if w is not None:
if len(args) > 1 and args[1] != "prev":
w.scroll_to_mark(prev=False)
else:
w.scroll_to_mark()

338
.config/kitty/search.py Normal file
View file

@ -0,0 +1,338 @@
import json
import re
import subprocess
from gettext import gettext as _
from pathlib import Path
from subprocess import PIPE, run
from kittens.tui.handler import Handler
from kittens.tui.line_edit import LineEdit
from kittens.tui.loop import Loop
from kittens.tui.operations import (
clear_screen,
cursor,
set_line_wrapping,
set_window_title,
styled,
)
from kitty.config import cached_values_for
from kitty.key_encoding import EventType
from kitty.typing import KeyEventType, ScreenSize
NON_SPACE_PATTERN = re.compile(r"\S+")
SPACE_PATTERN = re.compile(r"\s+")
SPACE_PATTERN_END = re.compile(r"\s+$")
SPACE_PATTERN_START = re.compile(r"^\s+")
NON_ALPHANUM_PATTERN = re.compile(r"[^\w\d]+")
NON_ALPHANUM_PATTERN_END = re.compile(r"[^\w\d]+$")
NON_ALPHANUM_PATTERN_START = re.compile(r"^[^\w\d]+")
ALPHANUM_PATTERN = re.compile(r"[\w\d]+")
def call_remote_control(args: list[str]) -> None:
subprocess.run(["kitty", "@", *args], capture_output=True)
def reindex(
text: str, pattern: re.Pattern[str], right: bool = False
) -> tuple[int, int]:
if not right:
m = pattern.search(text)
else:
matches = [x for x in pattern.finditer(text) if x]
if not matches:
raise ValueError
m = matches[-1]
if not m:
raise ValueError
return m.span()
SCROLLMARK_FILE = Path(__file__).parent.absolute() / "scroll_mark.py"
class Search(Handler):
def __init__(
self, cached_values: dict[str, str], window_ids: list[int], error: str = ""
) -> None:
self.cached_values = cached_values
self.window_ids = window_ids
self.error = error
self.line_edit = LineEdit()
last_search = cached_values.get("last_search", "")
self.line_edit.add_text(last_search)
self.text_marked = bool(last_search)
self.mode = cached_values.get("mode", "text")
self.update_prompt()
self.mark()
def update_prompt(self) -> None:
self.prompt = "~> " if self.mode == "regex" else "=> "
def init_terminal_state(self) -> None:
self.write(set_line_wrapping(False))
self.write(set_window_title(_("Search")))
def initialize(self) -> None:
self.init_terminal_state()
self.draw_screen()
def draw_screen(self) -> None:
self.write(clear_screen())
if self.window_ids:
input_text = self.line_edit.current_input
if self.text_marked:
self.line_edit.current_input = styled(input_text, reverse=True)
self.line_edit.write(self.write, self.prompt)
self.line_edit.current_input = input_text
if self.error:
with cursor(self.write):
self.print("")
for l in self.error.split("\n"):
self.print(l)
def refresh(self) -> None:
self.draw_screen()
self.mark()
def switch_mode(self) -> None:
if self.mode == "regex":
self.mode = "text"
else:
self.mode = "regex"
self.cached_values["mode"] = self.mode
self.update_prompt()
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
if self.text_marked:
self.text_marked = False
self.line_edit.clear()
self.line_edit.on_text(text, in_bracketed_paste)
self.refresh()
def on_key(self, key_event: KeyEventType) -> None:
if (
self.text_marked
and key_event.type == EventType.PRESS
and key_event.key
not in [
"TAB",
"LEFT_CONTROL",
"RIGHT_CONTROL",
"LEFT_ALT",
"RIGHT_ALT",
"LEFT_SHIFT",
"RIGHT_SHIFT",
"LEFT_SUPER",
"RIGHT_SUPER",
]
):
self.text_marked = False
self.refresh()
if self.line_edit.on_key(key_event):
self.refresh()
return
if key_event.matches("ctrl+u"):
self.line_edit.clear()
self.refresh()
elif key_event.matches("ctrl+a"):
self.line_edit.home()
self.refresh()
elif key_event.matches("ctrl+e"):
self.line_edit.end()
self.refresh()
elif key_event.matches("ctrl+backspace") or key_event.matches("ctrl+w"):
before, _ = self.line_edit.split_at_cursor()
try:
start, _ = reindex(before, SPACE_PATTERN_END, right=True)
except ValueError:
start = -1
try:
space = before[:start].rindex(" ")
except ValueError:
space = 0
self.line_edit.backspace(len(before) - space)
self.refresh()
elif key_event.matches("ctrl+left") or key_event.matches("ctrl+b"):
before, _ = self.line_edit.split_at_cursor()
try:
start, _ = reindex(before, SPACE_PATTERN_END, right=True)
except ValueError:
start = -1
try:
space = before[:start].rindex(" ")
except ValueError:
space = 0
self.line_edit.left(len(before) - space)
self.refresh()
elif key_event.matches("ctrl+right") or key_event.matches("ctrl+f"):
_, after = self.line_edit.split_at_cursor()
try:
_, end = reindex(after, SPACE_PATTERN_START)
except ValueError:
end = 0
try:
space = after[end:].index(" ") + 1
except ValueError:
space = len(after)
self.line_edit.right(space)
self.refresh()
elif key_event.matches("alt+backspace") or key_event.matches("alt+w"):
before, _ = self.line_edit.split_at_cursor()
try:
start, _ = reindex(before, NON_ALPHANUM_PATTERN_END, right=True)
except ValueError:
start = -1
else:
self.line_edit.backspace(len(before) - start)
self.refresh()
return
try:
start, _ = reindex(before, NON_ALPHANUM_PATTERN, right=True)
except ValueError:
self.line_edit.backspace(len(before))
self.refresh()
return
self.line_edit.backspace(len(before) - (start + 1))
self.refresh()
elif key_event.matches("alt+left") or key_event.matches("alt+b"):
before, _ = self.line_edit.split_at_cursor()
try:
start, _ = reindex(before, NON_ALPHANUM_PATTERN_END, right=True)
except ValueError:
start = -1
else:
self.line_edit.left(len(before) - start)
self.refresh()
return
try:
start, _ = reindex(before, NON_ALPHANUM_PATTERN, right=True)
except ValueError:
self.line_edit.left(len(before))
self.refresh()
return
self.line_edit.left(len(before) - (start + 1))
self.refresh()
elif key_event.matches("alt+right") or key_event.matches("alt+f"):
_, after = self.line_edit.split_at_cursor()
try:
_, end = reindex(after, NON_ALPHANUM_PATTERN_START)
except ValueError:
end = 0
else:
self.line_edit.right(end)
self.refresh()
return
try:
_, end = reindex(after, NON_ALPHANUM_PATTERN)
except ValueError:
self.line_edit.right(len(after))
self.refresh()
return
self.line_edit.right(end - 1)
self.refresh()
elif key_event.matches("tab"):
self.switch_mode()
self.refresh()
elif key_event.matches("up"):
for match_arg in self.match_args():
call_remote_control(["kitten", match_arg, str(SCROLLMARK_FILE)])
elif key_event.matches("down"):
for match_arg in self.match_args():
call_remote_control(["kitten", match_arg, str(SCROLLMARK_FILE), "next"])
elif key_event.matches("enter"):
self.quit(0)
elif key_event.matches("esc"):
self.quit(1)
def on_interrupt(self) -> None:
self.quit(1)
def on_eot(self) -> None:
self.quit(1)
def on_resize(self, screen_size: ScreenSize) -> None:
self.refresh()
def match_args(self) -> list[str]:
return [f"--match=id:{window_id}" for window_id in self.window_ids]
def mark(self) -> None:
if not self.window_ids:
return
text = self.line_edit.current_input
if text:
match_case = "i" if text.islower() else ""
match_type = match_case + self.mode
for match_arg in self.match_args():
try:
call_remote_control(
["create-marker", match_arg, match_type, "1", text]
)
except SystemExit:
self.remove_mark()
else:
self.remove_mark()
def remove_mark(self) -> None:
for match_arg in self.match_args():
call_remote_control(["remove-marker", match_arg])
def quit(self, return_code: int) -> None:
self.cached_values["last_search"] = self.line_edit.current_input
self.remove_mark()
if return_code:
for match_arg in self.match_args():
call_remote_control(["scroll-window", match_arg, "end"])
self.quit_loop(return_code)
def main(args: list[str]) -> None:
call_remote_control(
["resize-window", "--self", "--axis=vertical", "--increment", "-100"]
)
error = ""
if len(args) < 2 or not args[1].isdigit():
error = "Error: Window id must be provided as the first argument."
window_id = int(args[1])
window_ids = [window_id]
if len(args) > 2 and args[2] == "--all-windows":
ls_output = run(["kitty", "@", "ls"], stdout=PIPE)
ls_json = json.loads(ls_output.stdout.decode())
current_tab = None
for os_window in ls_json:
for tab in os_window["tabs"]:
for kitty_window in tab["windows"]:
if kitty_window["id"] == window_id:
current_tab = tab
if current_tab:
window_ids = [
w["id"] for w in current_tab["windows"] if not w["is_focused"]
]
else:
error = "Error: Could not find the window id provided."
loop = Loop()
with cached_values_for("search") as cached_values:
handler = Search(cached_values, window_ids, error)
loop.loop(handler)