364 lines
10 KiB
Python
364 lines
10 KiB
Python
import logging
|
|
from typing import Optional
|
|
|
|
from talon import Context, Module, actions, settings
|
|
|
|
mod = Module()
|
|
mod.setting(
|
|
"emacs_meta",
|
|
type=str,
|
|
default="esc",
|
|
desc="""What to use for the meta key in emacs. Defaults to 'esc', since that should work everywhere. Other options are 'alt' and 'cmd'.""",
|
|
)
|
|
|
|
mod.apps.emacs = "app.name: Emacs"
|
|
mod.apps.emacs = "app.name: emacs"
|
|
mod.apps.emacs = "app.name: /^GNU Emacs/"
|
|
mod.apps.emacs = """
|
|
os: mac
|
|
app.bundle: org.gnu.Emacs
|
|
"""
|
|
mod.apps.emacs = r"""
|
|
os: windows
|
|
app.exe: /^emacs\.exe$/i
|
|
"""
|
|
|
|
ctx = Context()
|
|
ctx.matches = "app: emacs"
|
|
|
|
|
|
def meta(keys):
|
|
m = settings.get("user.emacs_meta")
|
|
if m == "alt":
|
|
return " ".join("alt-" + k for k in keys.split())
|
|
elif m == "cmd":
|
|
return " ".join("cmd-" + k for k in keys.split())
|
|
elif m != "esc":
|
|
logging.error(
|
|
f"Unrecognized 'emacs_meta' setting: {m!r}. Falling back to 'esc'."
|
|
)
|
|
return "esc " + keys
|
|
|
|
|
|
def meta_fixup(k):
|
|
if k.startswith("meta-"):
|
|
k = meta(k[len("meta-") :])
|
|
elif "meta-" in k:
|
|
raise NotImplementedError("user.emacs_key(): please put meta- first")
|
|
return k
|
|
|
|
|
|
@mod.action_class
|
|
class Actions:
|
|
def emacs_meta(key: str):
|
|
"Presses some keys modified by Emacs' meta key."
|
|
actions.key(meta(key))
|
|
|
|
def emacs_key(keys: str):
|
|
"""
|
|
Presses some keys, translating 'meta-' prefix to the appropriate keys. For
|
|
example, if the setting user.emacs_meta = 'esc', user.emacs_key("meta-ctrl-a")
|
|
becomes key("esc ctrl-a").
|
|
"""
|
|
# TODO: handle corner-cases like key(" ") and key("ctrl- "), etc.
|
|
actions.key(" ".join(meta_fixup(k) for k in keys.split()))
|
|
|
|
def emacs_prefix(n: Optional[int] = None):
|
|
"Inputs a prefix argument."
|
|
if n is None:
|
|
# `M-x universal-argument` doesn't have the same effect as pressing the key.
|
|
prefix_key = actions.user.emacs_command_keybinding("universal-argument")
|
|
actions.key(prefix_key or "ctrl-u") # default to ctrl-u
|
|
else:
|
|
# Applying meta to each key can use fewer keypresses and 'works' in ansi-term
|
|
# mode.
|
|
actions.user.emacs_meta(" ".join(str(n)))
|
|
|
|
def emacs(command_name: str, prefix: Optional[int] = None):
|
|
"""
|
|
Runs the emacs command `command_name`. Defaults to using M-x, but may use
|
|
a key binding if known or rpc if available. Provides numeric prefix argument
|
|
`prefix` if specified.
|
|
"""
|
|
meta_x = actions.user.emacs_command_keybinding("execute-extended-command")
|
|
keys = actions.user.emacs_command_keybinding(command_name)
|
|
short_form = actions.user.emacs_command_short_form(command_name)
|
|
if prefix is not None:
|
|
actions.user.emacs_prefix(prefix)
|
|
if keys is not None:
|
|
actions.user.emacs_key(keys)
|
|
else:
|
|
actions.user.emacs_key(meta_x or "meta-x")
|
|
actions.insert(short_form or command_name)
|
|
actions.key("enter")
|
|
|
|
def emacs_help(key: str = None):
|
|
"Runs the emacs help command prefix, optionally followed by some keys."
|
|
# NB. f1 works in ansi-term mode; C-h doesn't.
|
|
actions.key("f1")
|
|
if key is not None:
|
|
actions.key(key)
|
|
|
|
|
|
@ctx.action_class("user")
|
|
class UserActions:
|
|
def cut_line():
|
|
actions.edit.line_start()
|
|
actions.user.emacs("kill-line", 1)
|
|
|
|
def split_window():
|
|
actions.user.emacs("split-window-below")
|
|
|
|
def split_window_vertically():
|
|
actions.user.emacs("split-window-below")
|
|
|
|
def split_window_up():
|
|
actions.user.emacs("split-window-below")
|
|
|
|
def split_window_down():
|
|
actions.user.emacs("split-window-below")
|
|
actions.user.emacs("other-window")
|
|
|
|
def split_window_horizontally():
|
|
actions.user.emacs("split-window-right")
|
|
|
|
def split_window_left():
|
|
actions.user.emacs("split-window-right")
|
|
|
|
def split_window_right():
|
|
actions.user.emacs("split-window-right")
|
|
actions.user.emacs("other-window")
|
|
|
|
def split_clear():
|
|
actions.user.emacs("delete-window")
|
|
|
|
def split_clear_all():
|
|
actions.user.emacs("delete-other-windows")
|
|
|
|
def split_reset():
|
|
actions.user.emacs("balance-windows")
|
|
|
|
def split_next():
|
|
actions.user.emacs("other-window")
|
|
|
|
def split_last():
|
|
actions.user.emacs("other-window", -1)
|
|
|
|
def split_flip():
|
|
# only works reliably if there are only two panes/windows.
|
|
actions.key("ctrl-x b enter ctrl-x o ctrl-x b enter")
|
|
actions.user.split_last()
|
|
actions.key("ctrl-x b enter ctrl-x o")
|
|
|
|
def select_range(line_start, line_end):
|
|
# Assumes transient mark mode.
|
|
actions.edit.jump_line(line_start)
|
|
actions.edit.jump_line(line_end + 1)
|
|
actions.user.emacs("exchange-point-and-mark")
|
|
|
|
# # Version that highlights without transient-mark-mode:
|
|
# def select_range(line_start, line_end):
|
|
# actions.edit.jump_line(line_end + 1)
|
|
# actions.key("ctrl-@ ctrl-@")
|
|
# actions.edit.jump_line(line_start)
|
|
|
|
# dictation_peek() probably won't work in a terminal. PRs welcome.
|
|
def dictation_peek(left, right):
|
|
# clobber transient selection if it exists
|
|
actions.key("space backspace")
|
|
before, after = None, None
|
|
if left:
|
|
actions.edit.extend_word_left()
|
|
before = actions.edit.selected_text()
|
|
actions.user.emacs("pop-to-mark-command")
|
|
if right:
|
|
actions.edit.extend_line_end()
|
|
after = actions.edit.selected_text()
|
|
actions.user.emacs("pop-to-mark-command")
|
|
return (before, after)
|
|
|
|
|
|
@ctx.action_class("edit")
|
|
class EditActions:
|
|
def save():
|
|
actions.user.emacs("save-buffer")
|
|
|
|
def save_all():
|
|
actions.user.emacs("save-some-buffers")
|
|
|
|
def copy():
|
|
actions.user.emacs("kill-ring-save")
|
|
|
|
def cut():
|
|
actions.user.emacs("kill-region")
|
|
|
|
def undo():
|
|
actions.user.emacs("undo")
|
|
|
|
def paste():
|
|
actions.user.emacs("yank")
|
|
|
|
def delete():
|
|
actions.user.emacs("kill-region")
|
|
|
|
def file_start():
|
|
actions.user.emacs("beginning-of-buffer")
|
|
|
|
def file_end():
|
|
actions.user.emacs("end-of-buffer")
|
|
|
|
# works for eg 'select to top', but not if preceded by other selections :(
|
|
def extend_file_start():
|
|
actions.user.emacs("beginning-of-buffer")
|
|
|
|
def extend_file_end():
|
|
actions.user.emacs("end-of-buffer")
|
|
|
|
def select_none():
|
|
actions.user.emacs("keyboard-quit")
|
|
|
|
def select_all():
|
|
actions.user.emacs("mark-whole-buffer")
|
|
# If you don't use transient-mark-mode, maybe do this:
|
|
# actions.key('ctrl-u ctrl-x ctrl-x')
|
|
|
|
def word_left():
|
|
actions.user.emacs("backward-word")
|
|
|
|
def word_right():
|
|
actions.user.emacs("forward-word")
|
|
|
|
def extend_word_left():
|
|
actions.user.emacs_meta("shift-b")
|
|
|
|
def extend_word_right():
|
|
actions.user.emacs_meta("shift-f")
|
|
|
|
def sentence_start():
|
|
actions.user.emacs("backward-sentence")
|
|
|
|
def sentence_end():
|
|
actions.user.emacs("forward-sentence")
|
|
|
|
def extend_sentence_start():
|
|
actions.user.emacs_meta("shift-a")
|
|
|
|
def extend_sentence_end():
|
|
actions.user.emacs_meta("shift-e")
|
|
|
|
def paragraph_start():
|
|
actions.user.emacs("backward-paragraph")
|
|
|
|
def paragraph_end():
|
|
actions.user.emacs("forward-paragraph")
|
|
|
|
def line_start():
|
|
actions.user.emacs("move-beginning-of-line")
|
|
|
|
def line_end():
|
|
actions.user.emacs("move-end-of-line")
|
|
|
|
def extend_line_start():
|
|
actions.key("shift-ctrl-a")
|
|
|
|
def extend_line_end():
|
|
actions.key("shift-ctrl-e")
|
|
|
|
def line_swap_down():
|
|
actions.key("down ctrl-x ctrl-t up")
|
|
|
|
def line_swap_up():
|
|
actions.key("ctrl-x ctrl-t up:2")
|
|
|
|
def delete_line():
|
|
actions.key("ctrl-a ctrl-k")
|
|
|
|
def line_clone():
|
|
actions.user.emacs_key("ctrl-a meta-1 ctrl-k ctrl-y ctrl-y up meta-m")
|
|
|
|
def jump_line(n):
|
|
actions.user.emacs("goto-line", n)
|
|
|
|
def select_line(n: int = None):
|
|
if n is not None:
|
|
actions.edit.jump_line(n)
|
|
else:
|
|
actions.edit.line_start()
|
|
actions.edit.extend_line_end()
|
|
actions.edit.extend_right()
|
|
# This makes it so the cursor is on the same line, which can make
|
|
# subsequent commands more convenient.
|
|
actions.user.emacs("exchange-point-and-mark")
|
|
|
|
def indent_more():
|
|
actions.user.emacs("indent-rigidly", 4)
|
|
|
|
def indent_less():
|
|
actions.user.emacs("indent-rigidly", -4)
|
|
|
|
# These all perform text-scale-adjust, which examines the actual key pressed, so can't
|
|
# be done with actions.user.emacs.
|
|
def zoom_in():
|
|
actions.key("ctrl-x ctrl-+")
|
|
|
|
def zoom_out():
|
|
actions.key("ctrl-x ctrl--")
|
|
|
|
def zoom_reset():
|
|
actions.key("ctrl-x ctrl-0")
|
|
|
|
# Some modes override ctrl-s/r to do something other than isearch-forward, so we
|
|
# deliberately don't use actions.user.emacs.
|
|
def find(text: str = None):
|
|
actions.key("ctrl-s")
|
|
if text:
|
|
actions.insert(text)
|
|
|
|
def find_next():
|
|
actions.key("ctrl-s")
|
|
|
|
def find_previous():
|
|
actions.key("ctrl-r")
|
|
|
|
|
|
@ctx.action_class("app")
|
|
class AppActions:
|
|
def window_open():
|
|
actions.user.emacs("make-frame-command")
|
|
|
|
def tab_next():
|
|
actions.user.emacs("tab-next")
|
|
|
|
def tab_previous():
|
|
actions.user.emacs("tab-previous")
|
|
|
|
def tab_close():
|
|
actions.user.emacs("tab-close")
|
|
|
|
def tab_reopen():
|
|
actions.user.emacs("tab-undo")
|
|
|
|
def tab_open():
|
|
actions.user.emacs("tab-new")
|
|
|
|
|
|
@ctx.action_class("code")
|
|
class CodeActions:
|
|
def toggle_comment():
|
|
actions.user.emacs("comment-dwim")
|
|
|
|
def language():
|
|
# Assumes win.filename() gives buffer name.
|
|
if "*scratch*" == actions.win.filename():
|
|
return "elisp"
|
|
return actions.next()
|
|
|
|
|
|
@ctx.action_class("win")
|
|
class WinActions:
|
|
# This assumes the title is/contains the filename.
|
|
# To do this, put this in init.el:
|
|
# (setq-default frame-title-format '((:eval (buffer-name (window-buffer (minibuffer-selected-window))))))
|
|
def filename():
|
|
return actions.win.title()
|