Skip to content

Commit

Permalink
add emacs support (#932)
Browse files Browse the repository at this point in the history
emacs support

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
rntz and pre-commit-ci[bot] authored Apr 8, 2023
1 parent afa96d6 commit 54acb67
Show file tree
Hide file tree
Showing 5 changed files with 1,017 additions and 0 deletions.
341 changes: 341 additions & 0 deletions apps/emacs/emacs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
import logging

from talon import Context, Module, actions

mod = Module()
setting_meta = 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 = """
os: mac
app.bundle: org.gnu.Emacs
"""

ctx = Context()
ctx.matches = "app: emacs"


def meta(keys):
m = setting_meta.get()
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_meta_x():
"Prompts user to enter a command name via execute-extended-command (M-x)."
actions.user.emacs_meta("x")

def emacs_prefix(n: int):
"Inputs a numeric prefix argument."
# Applying meta to each key can use fewer keypresses and 'works' in ansi-term
# mode.
actions.user.emacs_meta(" ".join(str(n)))
# # Alternative implementation using universal-argument (ctrl-u):
# actions.user.emacs("universal-argument")
# actions.key(" ".join(str(n)))

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-mark")
if right:
actions.edit.extend_line_end()
after = actions.edit.selected_text()
actions.user.emacs("pop-mark")
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()
Loading

0 comments on commit 54acb67

Please sign in to comment.