init commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Stefan Schneider
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,33 @@
|
||||
The draft window allows you to more easily edit prose style text via a task-specific UI.
|
||||
|
||||
# Usage
|
||||
|
||||
The main idea is that we have a Talon controlled text area where each word is labelled with a letter (called an anchor). You can use the anchors to indicate which word you want to operate on.
|
||||
|
||||
An session might go like this for example:
|
||||
|
||||
# Start with the text "this is a sentence with an elephant." in your editor or other textbox
|
||||
draft edit all # Select all the text in your editor and moves it to the draft window
|
||||
replace gust with error # Replaces the word corresponding with the red anchor 'g' (gust in community) with the word 'error'
|
||||
period # Add a full stop
|
||||
select each through fine # Select the words starting at the 'e' anchor and ending at 'f'
|
||||
say without # Insert the word 'without' (community)
|
||||
title word air # Make the word corresponding to the 'a' anchor capitalised
|
||||
draft submit # Type the text in your draft window back into your editor
|
||||
# End with the text "This is a sentence without error." in your editor or other textbox
|
||||
|
||||
Here's a video of me going through the above commands:
|
||||
|
||||

|
||||
|
||||
# Customising
|
||||
|
||||
If you want to change the display of the window you can do by adding some settings to one of your .talon files. See `settings.talon.example` for more details.
|
||||
|
||||
# Running tests
|
||||
|
||||
There are unit tests that you can run from the repository root like this (assuming your directory is called talon_draft_window):
|
||||
|
||||
(cd ../ && python -m unittest talon_draft_window.test_draft_ui)
|
||||
|
||||
The reason for the weirdness is because we have everything in the same directory and are doing relative imports.
|
||||
@@ -0,0 +1 @@
|
||||
# Used so we can use pytest to run this sub-package's tests
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,38 @@
|
||||
# These are available globally (in command mode)
|
||||
mode: command
|
||||
-
|
||||
^draft show:
|
||||
# Do this toggle so we can have focus when saying 'draft show'
|
||||
user.draft_hide()
|
||||
user.draft_show()
|
||||
|
||||
^draft show <user.draft_window_position>:
|
||||
# Do this toggle so we can have focus when saying 'draft show'
|
||||
user.draft_hide()
|
||||
user.draft_show()
|
||||
user.draft_named_move(draft_window_position)
|
||||
|
||||
^draft show small:
|
||||
# Do this toggle so we can have focus when saying 'draft show'
|
||||
user.draft_hide()
|
||||
user.draft_show()
|
||||
user.draft_resize(600, 200)
|
||||
|
||||
^draft show large:
|
||||
# Do this toggle so we can have focus when saying 'draft show'
|
||||
user.draft_hide()
|
||||
user.draft_show()
|
||||
user.draft_resize(800, 500)
|
||||
|
||||
^draft empty: user.draft_show("")
|
||||
|
||||
^draft edit:
|
||||
text = edit.selected_text()
|
||||
key(backspace)
|
||||
user.draft_show(text)
|
||||
|
||||
^draft edit all:
|
||||
edit.select_all()
|
||||
text = edit.selected_text()
|
||||
key(backspace)
|
||||
user.draft_show(text)
|
||||
@@ -0,0 +1,322 @@
|
||||
from typing import Optional
|
||||
|
||||
from talon import Context, Module, actions, settings, ui
|
||||
|
||||
from .draft_ui import DraftManager
|
||||
|
||||
mod = Module()
|
||||
|
||||
# ctx is for toggling the draft_window_showing variable
|
||||
# which lets you execute actions whenever the window is visible.
|
||||
ctx = Context()
|
||||
|
||||
# ctx_focused is active only when the draft window is focussed. This
|
||||
# lets you execute actions under that condition.
|
||||
ctx_focused = Context()
|
||||
ctx_focused.matches = r"""
|
||||
title: Talon Draft
|
||||
"""
|
||||
|
||||
mod.tag("draft_window_showing", desc="Tag set when draft window showing")
|
||||
mod.setting(
|
||||
"draft_window_theme",
|
||||
type=str,
|
||||
default="dark",
|
||||
desc="Sets the main colors of the window, one of 'dark' or 'light'",
|
||||
)
|
||||
mod.setting(
|
||||
"draft_window_label_size",
|
||||
type=int,
|
||||
default=20,
|
||||
desc="Sets the size of the word labels used in the draft window",
|
||||
)
|
||||
mod.setting(
|
||||
"draft_window_label_color",
|
||||
type=str,
|
||||
default=None,
|
||||
desc=(
|
||||
"Sets the color of the word labels used in the draft window. "
|
||||
"E.g. 00ff00 would be green"
|
||||
),
|
||||
)
|
||||
mod.setting(
|
||||
"draft_window_text_size",
|
||||
type=int,
|
||||
default=20,
|
||||
desc="Sets the size of the text used in the draft window",
|
||||
)
|
||||
|
||||
|
||||
draft_manager = DraftManager()
|
||||
|
||||
|
||||
# Update the styling of the draft window dynamically as user settings change
|
||||
def _update_draft_style(*args):
|
||||
draft_manager.set_styling(
|
||||
**{
|
||||
arg: settings.get(setting)
|
||||
for setting, arg in (
|
||||
("user.draft_window_theme", "theme"),
|
||||
("user.draft_window_label_size", "label_size"),
|
||||
("user.draft_window_label_color", "label_color"),
|
||||
("user.draft_window_text_size", "text_size"),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
settings.register("", _update_draft_style)
|
||||
|
||||
|
||||
@ctx_focused.action_class("user")
|
||||
class ContextSensitiveDictationActions:
|
||||
"""
|
||||
Override these actions to assist 'Smart dictation mode'.
|
||||
see https://github.com/talonhub/community/pull/356
|
||||
"""
|
||||
|
||||
def dictation_peek(left, right):
|
||||
area = draft_manager.area
|
||||
return (
|
||||
area[max(0, area.sel.left - 50) : area.sel.left],
|
||||
area[area.sel.right : area.sel.right + 50],
|
||||
)
|
||||
|
||||
def paste(text: str):
|
||||
# todo: remove once user.paste works reliably with the draft window
|
||||
actions.insert(text)
|
||||
|
||||
|
||||
@ctx_focused.action_class("edit")
|
||||
class EditActions:
|
||||
"""
|
||||
Make default edit actions more efficient.
|
||||
"""
|
||||
|
||||
def selected_text() -> str:
|
||||
area = draft_manager.area
|
||||
if area.sel:
|
||||
result = area[area.sel.left : area.sel.right]
|
||||
return result
|
||||
return ""
|
||||
|
||||
|
||||
from talon import cron
|
||||
|
||||
|
||||
class UndoWorkaround:
|
||||
"""
|
||||
Workaround for the experimental textarea's undo being character by character.
|
||||
This keeps a debounced undo history. Can be deleted once this todo item is
|
||||
fixed: https://github.com/talonvoice/talon/issues/254#issuecomment-789149734
|
||||
"""
|
||||
|
||||
# Set this to False if you want to turn it off, or just delete all references
|
||||
# to this class
|
||||
enable_workaround = True
|
||||
|
||||
# Stack of (text_value, selection) tuples representing the undo stack
|
||||
undo_stack = []
|
||||
# Stack of (text_value, selection) tuples representing the redo stack
|
||||
redo_stack = []
|
||||
# Used by the timer to check when the text has stopped changing
|
||||
pending_undo = None
|
||||
|
||||
# timer handle
|
||||
timer_handle = None
|
||||
|
||||
@classmethod
|
||||
def start_logger(cls, reset_undo_stack: bool):
|
||||
if reset_undo_stack:
|
||||
cls.undo_stack = []
|
||||
cls.redo_stack = []
|
||||
|
||||
cls.stop_logger()
|
||||
cls.timer_handle = cron.interval("500ms", cls._log_changes)
|
||||
|
||||
@classmethod
|
||||
def stop_logger(cls):
|
||||
if cls.timer_handle is not None:
|
||||
cron.cancel(cls.timer_handle)
|
||||
cls.timer_handle = None
|
||||
cls.pending_undo = None
|
||||
|
||||
@classmethod
|
||||
def perform_undo(cls):
|
||||
if len(cls.undo_stack) == 0:
|
||||
return
|
||||
|
||||
curr_text = draft_manager.area.value
|
||||
curr_sel = (draft_manager.area.sel.left, draft_manager.area.sel.right)
|
||||
text, sel = cls.undo_stack[-1]
|
||||
if text == curr_text:
|
||||
cls.undo_stack.pop()
|
||||
if len(cls.undo_stack) == 0:
|
||||
return
|
||||
|
||||
# Most of the time (unless user has only just finished updating) the
|
||||
# top of the stack will have the same contents as the text area. In
|
||||
# this case pop again to get a bit lower. We should never have the
|
||||
# same text twice, hence we don't need a loop.
|
||||
text, sel = cls.undo_stack[-1]
|
||||
|
||||
# Remember the current state in the redo stack
|
||||
cls.redo_stack.append((curr_text, curr_sel))
|
||||
draft_manager.area.value = text
|
||||
draft_manager.area.sel = sel
|
||||
|
||||
cls.pending_undo = (text, sel)
|
||||
|
||||
@classmethod
|
||||
def perform_redo(cls):
|
||||
if len(cls.redo_stack) == 0:
|
||||
return
|
||||
|
||||
text, sel = cls.redo_stack.pop()
|
||||
|
||||
draft_manager.area.value = text
|
||||
draft_manager.area.sel = sel
|
||||
|
||||
cls.pending_undo = (text, sel)
|
||||
cls.undo_stack.append((text, sel))
|
||||
|
||||
@classmethod
|
||||
def _log_changes(cls):
|
||||
"""
|
||||
If the text and cursor position hasn't changed for two interval iterations
|
||||
(1s) and the undo stack doesn't match the current state, then add to the stack.
|
||||
"""
|
||||
|
||||
curr_val = draft_manager.area.value
|
||||
# Turn the Span into a tuple, because we can't == Spans
|
||||
curr_sel = (draft_manager.area.sel.left, draft_manager.area.sel.right)
|
||||
curr_state = (curr_val, curr_sel)
|
||||
|
||||
state_stack_mismatch = (
|
||||
len(cls.undo_stack) == 0
|
||||
or
|
||||
# Only want to update the undo stack if the value has changed, not just
|
||||
# the selection
|
||||
curr_state[0] != cls.undo_stack[-1][0]
|
||||
)
|
||||
|
||||
if cls.pending_undo == curr_state and state_stack_mismatch:
|
||||
cls.undo_stack.append(curr_state)
|
||||
# Clear out the redo stack because we've changed the text
|
||||
cls.redo_stack = []
|
||||
elif cls.pending_undo != curr_state:
|
||||
cls.pending_undo = curr_state
|
||||
elif not state_stack_mismatch and len(cls.undo_stack) > 0:
|
||||
# Remember the cursor position in the undo stack for the current text value
|
||||
cls.undo_stack[-1] = (cls.undo_stack[-1][0], curr_sel)
|
||||
else:
|
||||
# The text area text is not changing, do nothing
|
||||
pass
|
||||
|
||||
|
||||
if UndoWorkaround.enable_workaround:
|
||||
ctx_focused.action("edit.undo")(UndoWorkaround.perform_undo)
|
||||
ctx_focused.action("edit.redo")(UndoWorkaround.perform_redo)
|
||||
|
||||
|
||||
@mod.action_class
|
||||
class Actions:
|
||||
def draft_show(text: Optional[str] = None):
|
||||
"""
|
||||
Shows draft window
|
||||
"""
|
||||
|
||||
draft_manager.show(text)
|
||||
UndoWorkaround.start_logger(text is not None)
|
||||
ctx.tags = ["user.draft_window_showing"]
|
||||
|
||||
def draft_hide():
|
||||
"""
|
||||
Hides draft window
|
||||
"""
|
||||
|
||||
draft_manager.hide()
|
||||
UndoWorkaround.stop_logger()
|
||||
ctx.tags = []
|
||||
|
||||
def draft_select(
|
||||
start_anchor: str, end_anchor: str = "", include_trailing_whitespace: int = 0
|
||||
):
|
||||
"""
|
||||
Selects text in the draft window
|
||||
"""
|
||||
|
||||
draft_manager.select_text(
|
||||
start_anchor,
|
||||
end_anchor=None if end_anchor == "" else end_anchor,
|
||||
include_trailing_whitespace=include_trailing_whitespace == 1,
|
||||
)
|
||||
|
||||
def draft_position_caret(anchor: str, after: int = 0):
|
||||
"""
|
||||
Positions the caret in the draft window
|
||||
"""
|
||||
|
||||
draft_manager.position_caret(anchor, after=after == 1)
|
||||
|
||||
def draft_get_text() -> str:
|
||||
"""
|
||||
Returns the text in the draft window
|
||||
"""
|
||||
|
||||
return draft_manager.get_text()
|
||||
|
||||
def draft_resize(width: int, height: int):
|
||||
"""
|
||||
Resize the draft window.
|
||||
"""
|
||||
|
||||
draft_manager.reposition(width=width, height=height)
|
||||
|
||||
def draft_named_move(name: str, screen_number: Optional[int] = None):
|
||||
"""
|
||||
Lets you move the window to the top, bottom, left, right, or middle
|
||||
of the screen.
|
||||
"""
|
||||
|
||||
screen = ui.screens()[screen_number or 0]
|
||||
window_rect = draft_manager.get_rect()
|
||||
xpos = (screen.width - window_rect.width) / 2
|
||||
ypos = (screen.height - window_rect.height) / 2
|
||||
|
||||
if name == "top":
|
||||
ypos = 50
|
||||
elif name == "bottom":
|
||||
ypos = screen.height - window_rect.height - 50
|
||||
elif name == "left":
|
||||
xpos = 50
|
||||
elif name == "right":
|
||||
xpos = screen.width - window_rect.width - 50
|
||||
elif name == "middle":
|
||||
# That's the default values
|
||||
pass
|
||||
|
||||
# Adjust for the fact that the screen may not be at 0,0.
|
||||
xpos += screen.x
|
||||
ypos += screen.y
|
||||
draft_manager.reposition(xpos=xpos, ypos=ypos)
|
||||
|
||||
|
||||
# Some capture groups we need
|
||||
|
||||
|
||||
@mod.capture(rule="{self.letter}+")
|
||||
def draft_anchor(m) -> str:
|
||||
"""
|
||||
An anchor (string of letters)
|
||||
"""
|
||||
return "".join(m)
|
||||
|
||||
|
||||
@mod.capture(rule="(top|bottom|left|right|middle)")
|
||||
def draft_window_position(m) -> str:
|
||||
"""
|
||||
One of the named positions you can move the window to
|
||||
"""
|
||||
|
||||
return "".join(m)
|
||||
@@ -0,0 +1,203 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from talon.experimental.textarea import (
|
||||
DarkThemeLabels,
|
||||
LightThemeLabels,
|
||||
Span,
|
||||
TextArea,
|
||||
)
|
||||
|
||||
word_matcher = re.compile(r"([^\s]+)(\s*)")
|
||||
|
||||
|
||||
def calculate_text_anchors(text, cursor_position, anchor_labels=None):
|
||||
"""
|
||||
Produces an iterator of (anchor, start_word_index, end_word_index, last_space_index)
|
||||
tuples from the given text. Each tuple indicates a particular point you may want to
|
||||
reference when editing along with some useful ranges you may want to operate on.
|
||||
|
||||
- text is the text you want to process.
|
||||
- cursor_position is the current position of the cursor, anchors will be placed around
|
||||
this.
|
||||
- anchor_labels is a list of characters you want to use for your labels.
|
||||
- *index is just a character offset from the start of the string (e.g. the first character is at index 0)
|
||||
- end_word_index is the index of the character after the last one included in the
|
||||
anchor. That is, you can use it with a slice directly like [start:end]
|
||||
- anchor is a short piece of text you can use to identify it (e.g. 'a', or '1').
|
||||
"""
|
||||
anchor_labels = anchor_labels or "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
if len(text) == 0:
|
||||
return []
|
||||
|
||||
# Find all the word spans
|
||||
matches = []
|
||||
cursor_idx = None
|
||||
for match in word_matcher.finditer(text):
|
||||
matches.append((match.start(), match.end() - len(match.group(2)), match.end()))
|
||||
if matches[-1][0] <= cursor_position and matches[-1][2] >= cursor_position:
|
||||
cursor_idx = len(matches) - 1
|
||||
|
||||
# Now work out what range of those matches are getting an anchor. The aim is
|
||||
# to centre the anchors around the cursor position, but also to use all the
|
||||
# anchors.
|
||||
anchors_before_cursor = len(anchor_labels) // 2
|
||||
anchor_start_idx = max(0, cursor_idx - anchors_before_cursor)
|
||||
anchor_end_idx = min(len(matches), anchor_start_idx + len(anchor_labels))
|
||||
anchor_start_idx = max(0, anchor_end_idx - len(anchor_labels))
|
||||
|
||||
# Now add anchors to the selected matches
|
||||
for i, anchor in zip(range(anchor_start_idx, anchor_end_idx), anchor_labels):
|
||||
word_start, word_end, whitespace_end = matches[i]
|
||||
yield (anchor, word_start, word_end, whitespace_end)
|
||||
|
||||
|
||||
class DraftManager:
|
||||
"""
|
||||
API to the draft window
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.area = TextArea()
|
||||
self.area.title = "Talon Draft"
|
||||
self.area.value = ""
|
||||
self.area.register("label", self._update_labels)
|
||||
self.set_styling()
|
||||
|
||||
def set_styling(self, theme="dark", text_size=20, label_size=20, label_color=None):
|
||||
"""
|
||||
Allow settings the style of the draft window. Will dynamically
|
||||
update the style based on the passed in parameters.
|
||||
"""
|
||||
|
||||
area_theme = DarkThemeLabels if theme == "dark" else LightThemeLabels
|
||||
theme_changes = {
|
||||
"text_size": text_size,
|
||||
"label_size": label_size,
|
||||
}
|
||||
if label_color is not None:
|
||||
theme_changes["label"] = label_color
|
||||
self.area.theme = area_theme(**theme_changes)
|
||||
|
||||
def show(self, text: Optional[str] = None):
|
||||
"""
|
||||
Show the window. If text is None then keep the old contents,
|
||||
otherwise set the text to the given value.
|
||||
"""
|
||||
|
||||
if text is not None:
|
||||
self.area.value = text
|
||||
self.area.show()
|
||||
|
||||
def hide(self):
|
||||
"""
|
||||
Hide the window.
|
||||
"""
|
||||
|
||||
self.area.hide()
|
||||
|
||||
def get_text(self) -> str:
|
||||
"""
|
||||
Gets the context of the text area
|
||||
"""
|
||||
|
||||
return self.area.value
|
||||
|
||||
def get_rect(self) -> "talon.types.Rect":
|
||||
"""
|
||||
Get the Rect for the window
|
||||
"""
|
||||
|
||||
return self.area.rect
|
||||
|
||||
def reposition(
|
||||
self,
|
||||
xpos: Optional[int] = None,
|
||||
ypos: Optional[int] = None,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Move the window or resize it without having to change all properties.
|
||||
"""
|
||||
|
||||
rect = self.area.rect
|
||||
if xpos is not None:
|
||||
rect.x = xpos
|
||||
|
||||
if ypos is not None:
|
||||
rect.y = ypos
|
||||
|
||||
if width is not None:
|
||||
rect.width = width
|
||||
|
||||
if height is not None:
|
||||
rect.height = height
|
||||
|
||||
self.area.rect = rect
|
||||
|
||||
def select_text(
|
||||
self, start_anchor, end_anchor=None, include_trailing_whitespace=False
|
||||
):
|
||||
"""
|
||||
Selects the word corresponding to start_anchor. If end_anchor supplied, selects
|
||||
from start_anchor to the end of end_anchor. If include_trailing_whitespace=True
|
||||
then also selects trailing space characters (useful for delete).
|
||||
"""
|
||||
|
||||
start_index, end_index, last_space_index = self.anchor_to_range(start_anchor)
|
||||
if end_anchor is not None:
|
||||
_, end_index, last_space_index = self.anchor_to_range(end_anchor)
|
||||
|
||||
if include_trailing_whitespace:
|
||||
end_index = last_space_index
|
||||
|
||||
self.area.sel = Span(start_index, end_index)
|
||||
|
||||
def position_caret(self, anchor, after=False):
|
||||
"""
|
||||
Positions the caret before the given anchor. If after=True position it directly after.
|
||||
"""
|
||||
|
||||
start_index, end_index, _ = self.anchor_to_range(anchor)
|
||||
index = end_index if after else start_index
|
||||
|
||||
self.area.sel = index
|
||||
|
||||
def anchor_to_range(self, anchor):
|
||||
anchors_data = calculate_text_anchors(
|
||||
self._get_visible_text(), self.area.sel.left
|
||||
)
|
||||
for loop_anchor, start_index, end_index, last_space_index in anchors_data:
|
||||
if anchor == loop_anchor:
|
||||
return (start_index, end_index, last_space_index)
|
||||
|
||||
raise RuntimeError(f"Couldn't find anchor {anchor}")
|
||||
|
||||
def _update_labels(self, _visible_text):
|
||||
"""
|
||||
Updates the position of the labels displayed on top of each word
|
||||
"""
|
||||
|
||||
anchors_data = calculate_text_anchors(
|
||||
self._get_visible_text(), self.area.sel.left
|
||||
)
|
||||
return [
|
||||
(Span(start_index, end_index), anchor)
|
||||
for anchor, start_index, end_index, _ in anchors_data
|
||||
]
|
||||
|
||||
def _get_visible_text(self):
|
||||
# Placeholder for a future method of getting this
|
||||
return self.area.value
|
||||
|
||||
|
||||
if False:
|
||||
# Some code for testing, change above False to True and edit as desired
|
||||
draft_manager = DraftManager()
|
||||
draft_manager.show(
|
||||
"This is a line of text\nand another line of text and some more text so that the line gets so long that it wraps a bit.\nAnd a final sentence"
|
||||
)
|
||||
draft_manager.reposition(xpos=100, ypos=100)
|
||||
draft_manager.select_text("c")
|
||||
@@ -0,0 +1,47 @@
|
||||
# These are active when we have focus on the draft window
|
||||
title: Talon Draft
|
||||
-
|
||||
settings():
|
||||
# Enable 'Smart dictation mode', see https://github.com/talonhub/community/pull/356
|
||||
user.context_sensitive_dictation = true
|
||||
|
||||
# Replace a single word with a phrase
|
||||
replace <user.draft_anchor> with <user.text>:
|
||||
user.draft_select("{draft_anchor}")
|
||||
result = user.formatted_text(text, "NOOP")
|
||||
insert(result)
|
||||
|
||||
# Position cursor before word
|
||||
(pre | cursor | cursor before) <user.draft_anchor>:
|
||||
user.draft_position_caret("{draft_anchor}")
|
||||
|
||||
# Position cursor after word
|
||||
(post | cursor after) <user.draft_anchor>:
|
||||
user.draft_position_caret("{draft_anchor}", 1)
|
||||
|
||||
# Select a whole word
|
||||
(take | select) <user.draft_anchor>: user.draft_select("{draft_anchor}")
|
||||
|
||||
# Select a range of words
|
||||
(take | select) <user.draft_anchor> (through | past) <user.draft_anchor>:
|
||||
user.draft_select("{draft_anchor_1}", "{draft_anchor_2}")
|
||||
|
||||
# Delete a word
|
||||
(change | chuck | clear) <user.draft_anchor>:
|
||||
user.draft_select("{draft_anchor}", "", 1)
|
||||
key(backspace)
|
||||
|
||||
# Delete a range of words
|
||||
(change | chuck | clear) <user.draft_anchor> (through | past) <user.draft_anchor>:
|
||||
user.draft_select(draft_anchor_1, draft_anchor_2, 1)
|
||||
key(backspace)
|
||||
|
||||
# reformat word
|
||||
<user.formatters> word <user.draft_anchor>:
|
||||
user.draft_select("{draft_anchor}", "", 1)
|
||||
user.formatters_reformat_selection(user.formatters)
|
||||
|
||||
# reformat range
|
||||
<user.formatters> <user.draft_anchor> (through | past) <user.draft_anchor>:
|
||||
user.draft_select(draft_anchor_1, draft_anchor_2, 1)
|
||||
user.formatters_reformat_selection(user.formatters)
|
||||
@@ -0,0 +1,12 @@
|
||||
# These are available when the draft window is open, but not necessarily focussed
|
||||
tag: user.draft_window_showing
|
||||
-
|
||||
draft hide: user.draft_hide()
|
||||
|
||||
draft submit:
|
||||
content = user.draft_get_text()
|
||||
user.draft_hide()
|
||||
insert(content)
|
||||
# user.paste may be somewhat faster, but seems to be unreliable on MacOSX, see
|
||||
# https://github.com/talonvoice/talon/issues/254#issuecomment-789355238
|
||||
# user.paste(content)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Put some settings like this in one of your Talon files to override the styling
|
||||
# of the draft window.
|
||||
-
|
||||
settings():
|
||||
user.draft_window_theme = "dark" # or light
|
||||
user.draft_window_text_size = 20
|
||||
user.draft_window_label_size = 20
|
||||
user.draft_window_label_color = "ff0000" # Any hex code RGB value, e.g. this is red
|
||||
@@ -0,0 +1,56 @@
|
||||
import talon
|
||||
|
||||
if hasattr(talon, "test_mode"):
|
||||
# Only include this when we're running tests
|
||||
|
||||
pass
|
||||
|
||||
from .draft_ui import calculate_text_anchors
|
||||
|
||||
def test_finds_anchors():
|
||||
examples = [
|
||||
("one-word", [("a", 0, 8, 8)]),
|
||||
("two words", [("a", 0, 3, 4), ("b", 4, 9, 9)]),
|
||||
("two\nwords", [("a", 0, 3, 4), ("b", 4, 9, 9)]),
|
||||
]
|
||||
anchor_labels = ["a", "b"]
|
||||
for text, expected in examples:
|
||||
# Given an example
|
||||
|
||||
# When we calculate the result and turn it into a list
|
||||
result = list(calculate_text_anchors(text, 0, anchor_labels=anchor_labels))
|
||||
|
||||
# Then it matches what we expect
|
||||
assert result == expected, text
|
||||
|
||||
def test_positions_anchors_around_cursor():
|
||||
# In these examples the cursor is at the asterisk which is stripped by the test
|
||||
# code. Indicies after the asterisk have to take this into account.
|
||||
examples = [
|
||||
("one*-word", [("a", 0, 8, 8)]),
|
||||
("one-word*", [("a", 0, 8, 8)]),
|
||||
("the three words*", [("a", 0, 3, 4), ("b", 4, 9, 10), ("c", 10, 15, 15)]),
|
||||
("*the three words", [("a", 0, 3, 4), ("b", 4, 9, 10), ("c", 10, 15, 15)]),
|
||||
(
|
||||
"too many* words for the number of anchors",
|
||||
[("a", 0, 3, 4), ("b", 4, 8, 9), ("c", 9, 14, 15)],
|
||||
),
|
||||
(
|
||||
"too many words fo*r the number of anchors",
|
||||
[("a", 9, 14, 15), ("b", 15, 18, 19), ("c", 19, 22, 23)],
|
||||
),
|
||||
]
|
||||
anchor_labels = ["a", "b", "c"]
|
||||
|
||||
for text_with_cursor, expected in examples:
|
||||
# Given an example
|
||||
cursor_pos = text_with_cursor.index("*")
|
||||
text = text_with_cursor.replace("*", "")
|
||||
|
||||
# When we calculate the result and turn it into a list
|
||||
result = list(
|
||||
calculate_text_anchors(text, cursor_pos, anchor_labels=anchor_labels)
|
||||
)
|
||||
|
||||
# Then it matches what we expect
|
||||
assert result == expected, text
|
||||
Reference in New Issue
Block a user