mirror of
https://github.com/CrispyConductor/tmux-copy-toolkit.git
synced 2026-09-10 04:06:14 -04:00
Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c80c2c0680 | ||
|
|
5faa1c4833 | ||
|
|
89fec42fe8 | ||
|
|
79b7cb64bb | ||
|
|
0705681ee4 | ||
|
|
8f24223ca5 | ||
|
|
289e1864ca | ||
|
|
626c408c7d |
27
README.md
27
README.md
|
|
@ -14,6 +14,7 @@ lines, panes, zooming, etc.
|
|||
* [easycopy](#easycopy)
|
||||
* [quickcopy](#quickcopy)
|
||||
* [quickopen](#quickopen)
|
||||
* [linecopy](#linecopy)
|
||||
* [Requirements](#requirements)
|
||||
* [Installation](#installation)
|
||||
* [Manual Install](#manual-install)
|
||||
|
|
@ -25,7 +26,7 @@ lines, panes, zooming, etc.
|
|||
|
||||
## Features
|
||||
|
||||
There are 4 major modes/features:
|
||||
There are 5 major modes/features:
|
||||
|
||||
### easymotion
|
||||
|
||||
|
|
@ -82,6 +83,25 @@ except that only openable things are highlighted. Selecting one invokes
|
|||
to import X environment variables from external source to ensure the command
|
||||
can reach X.
|
||||
|
||||
### linecopy
|
||||
|
||||
This mode allows copying blocks of full lines, or a single line; essentially a
|
||||
faster version of easycopy when the start and end points are both line boundaries.
|
||||
|
||||
To use, hit `Ctrl-b` `W` to activate linecopy mode. The beginning of each terminal
|
||||
line is replaced by the label string. Key in a label string to select the first line
|
||||
to copy, then the label string for the last line to copy (inclusive). The block of
|
||||
lines will then be copied.
|
||||
|
||||
To copy a single line, key in that line's label using uppercase letters. In this
|
||||
case, do not enter a second label. This will not work if there are any capital
|
||||
letters defined in the label chars. Single-line copies can also be performed
|
||||
using quickcopy.
|
||||
|
||||
Line beginnings are assigned for all terminal lines, including wrapped lines; but
|
||||
the end of a line always goes to the end of a "logical" line (a hard line break).
|
||||
This allows both easily copying wrapped lines and copying some partial lines.
|
||||
|
||||
## Requirements
|
||||
|
||||
- python3
|
||||
|
|
@ -134,6 +154,7 @@ copy | easymotion lines action (all lines) | `S` `n`
|
|||
any | easycopy | `<Prefix>` `S`
|
||||
any | quickcopy | `<Prefix>` `Q`
|
||||
any | quickopen | `<Prefix>` `P`
|
||||
any | linecopy | `<Prefix>` `W`
|
||||
|
||||
## Options
|
||||
|
||||
|
|
@ -162,6 +183,10 @@ Option | Default | Description
|
|||
`@copytk-flash-only-one` | `on` | In quickcopy mode, if there is more than one instance of the copied text on-screen, this is whether to flash all occurrences of the text or just one.
|
||||
`@copytk-quickopen-env-file` | `~/.tmux-copytk-env` | Path to a file containing newlike-separated `KEY=VALUE` environment variables. These are added to the environment for running the open command. Generation of this file can be automated in your shellrc.
|
||||
`@copytk-quickopen-open-command` | `xdg-open` on Linux, `open` on Mac | Command to run to open selected blocks in quickopen. The selected text is passed as an argument.
|
||||
`@copytk-color-highlight` | `green:yellow` | The color to use for highlighted matches, in the form `foreground`:`background`. Valid names are: `none`, `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`
|
||||
`@copytk-color-labelchar` | `red:none` | The color to use for the first/active label character.
|
||||
`@copytk-color-labelchar2` | `yellow:none` | The color to use for the second and subsequent label characters.
|
||||
`@copytk-color-message` | `red:none` | The color to use for the status message.
|
||||
|
||||
### quickcopy/quickopen matches
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ def make_path_regexes():
|
|||
# paths with at least 3 elements, and never in the first or last element. There cannot
|
||||
# be more than one consecutive space, and it cannot be at the beginning or end of the
|
||||
# element.
|
||||
edge_delimiters = r'[][\s:=,#$"{}<>()' + "'" + ']'
|
||||
edge_delimiters_w_slash = r'[][\s:=,#$"{}<>()/' + "'" + ']'
|
||||
edge_delimiters = r'[][\s:=,#$"{}<>()`' + "'" + ']'
|
||||
edge_delimiters_w_slash = r'[][\s:=,#$"{}<>()`/' + "'" + ']'
|
||||
leader = r'(?:^|' + edge_delimiters + ')'
|
||||
leader_w_slash = r'(?:^|' + edge_delimiters_w_slash + ')'
|
||||
follower = r'(?:$|' + edge_delimiters + ')'
|
||||
|
|
|
|||
204
copytk.py
204
copytk.py
|
|
@ -39,11 +39,11 @@ match_expr_presets = {
|
|||
# matches common types of urls and things that look like urls
|
||||
'urls': r'(?:^|[][\s:=,#"{}()'+"'"+r'])([a-zA-Z][a-zA-Z0-9]{1,5}://(?:[a-zA-Z0-9_]+(?::[a-zA-Z0-9_-]+)?@)?(?:(?:[a-zA-Z0-9][\w-]*\.)*[a-zA-Z][\w-]*|(?:[0-2]?[0-9]{1,2}\.){3}[0-2]?[0-9]{1,2})(?::[0-9]{1,5})?(?:/(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?/?)?(?:\?(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?&)*(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?)?)?(?:#(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?&)*(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?)?)?)(?:$|[][\s:=,#"{}()'+"'"+r'])',
|
||||
# Unix and window style absolute paths
|
||||
'abspaths': r'(?:^|[][\s:=,#$"{}<>()'+"'"+r'])((?:/|~/|[A-Z]:[\\/])(?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})[\\/])*(?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})\.[a-zA-Z0-9]{1,6}|(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/]?))(?:$|[][\s:=,#$"{}<>()'+"'"+r'])',
|
||||
'abspaths': r'(?:^|[][\s:=,#$"{}<>()`'+"'"+r'])((?:/|~/|[A-Z]:[\\/])(?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})[\\/])*(?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})\.[a-zA-Z0-9]{1,6}|(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/]?))(?:$|[][\s:=,#$"{}<>()`'+"'"+r'])',
|
||||
# Absolute or relative paths
|
||||
'paths': r'(?:^|[][\s:=,#$"{}<>()'+"'"+r'])((?:(?:/|~/|[A-Z]:[\\/])(?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})[\\/])*(?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})\.[a-zA-Z0-9]{1,6}|(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/]?)|(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/](?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/]?|(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})\.[a-zA-Z0-9]{1,6})|(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/](?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})[\\/])+(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/]?|(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})\.[a-zA-Z0-9]{1,6})))(?:$|[][\s:=,#$"{}<>()'+"'"+r'])',
|
||||
'paths': r'(?:^|[][\s:=,#$"{}<>()`'+"'"+r'])((?:(?:/|~/|[A-Z]:[\\/])(?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})[\\/])*(?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})\.[a-zA-Z0-9]{1,6}|(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/]?)|(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/](?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/]?|(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})\.[a-zA-Z0-9]{1,6})|(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/](?:(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})[\\/])+(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)[\\/]?|(?:(?:[a-zA-Z0-9_-]{1,80}|\.|\.\.)|[a-zA-Z0-9_-]{1,60}\\? [a-zA-Z0-9_-]{1,60})\.[a-zA-Z0-9]{1,6})))(?:$|[][\s:=,#$"{}<>()`'+"'"+r'])',
|
||||
# Isolated filenames without paths
|
||||
'filenames': r'(?:^|[][\s:=,#$"{}<>()/'+"'"+r'])([a-zA-Z0-9_-]{1,80}\.[a-zA-Z][a-zA-Z0-9]{0,5})(?:$|[][\s:=,#$"{}<>()'+"'"+r'])'
|
||||
'filenames': r'(?:^|[][\s:=,#$"{}<>()`/'+"'"+r'])([a-zA-Z0-9_-]{1,80}\.[a-zA-Z][a-zA-Z0-9]{0,5})(?:$|[][\s:=,#$"{}<>()`'+"'"+r'])',
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -106,28 +106,20 @@ def runtmux(args, one=False, lines=False, noblanklines=False, sendstdin=None):
|
|||
return dlines[0] if len(dlines) > 0 else ''
|
||||
return dlines if lines else data
|
||||
|
||||
def runshellcommand(command, one=False, lines=False, noblanklines=False, sendstdin=None, raisenonzero=True):
|
||||
def runshellcommand(command, sendstdin=None, raisenonzero=True):
|
||||
log('run shell command: ' + command, time=True)
|
||||
with subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
executable='/bin/bash',
|
||||
stdin=subprocess.PIPE if sendstdin != None else subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE
|
||||
stdout=subprocess.DEVNULL
|
||||
) as proc:
|
||||
if sendstdin != None and isinstance(sendstdin, str):
|
||||
sendstdin = bytearray(sendstdin, 'utf8')
|
||||
recvstdout, _ = proc.communicate(input=sendstdin)
|
||||
proc.communicate(input=sendstdin)
|
||||
if proc.returncode != 0 and raisenonzero:
|
||||
raise Exception(f'Command {command} returned exit code {proc.returncode}')
|
||||
data = recvstdout.decode('utf8')
|
||||
if one or lines: # return list of lines
|
||||
dlines = data.split('\n')
|
||||
if not one and noblanklines:
|
||||
dlines = [ l for l in dlines if len(l) > 0 ]
|
||||
if one: # single-line
|
||||
return dlines[0] if len(dlines) > 0 else ''
|
||||
return dlines if lines else data
|
||||
|
||||
|
||||
def runtmuxmulti(argsets):
|
||||
|
|
@ -218,6 +210,33 @@ def get_tmux_option_key_curses(name, default=None, optmode='g', aslist=False):
|
|||
else:
|
||||
return remap.get(v, v)
|
||||
|
||||
def get_tmux_option_color_pair_curses(name, default_fg=-1, default_bg=-1):
|
||||
strmap = {
|
||||
'none': -1,
|
||||
'black': curses.COLOR_BLACK,
|
||||
'red': curses.COLOR_RED,
|
||||
'green': curses.COLOR_GREEN,
|
||||
'yellow': curses.COLOR_YELLOW,
|
||||
'blue': curses.COLOR_BLUE,
|
||||
'magenta': curses.COLOR_MAGENTA,
|
||||
'cyan': curses.COLOR_CYAN,
|
||||
'white': curses.COLOR_WHITE
|
||||
}
|
||||
v = get_tmux_option(name)
|
||||
if v == None:
|
||||
return (default_fg, default_bg)
|
||||
parts = v.lower().split(':')
|
||||
if parts[0] not in strmap:
|
||||
raise Exception(f'Invalid color {parts[0]}')
|
||||
fg = strmap[parts[0]]
|
||||
if len(parts) > 1:
|
||||
if parts[1] not in strmap:
|
||||
raise Exception(f'Invalid color {parts[1]}')
|
||||
bg = strmap[parts[1]]
|
||||
else:
|
||||
bg = default_bg
|
||||
return (fg, bg)
|
||||
|
||||
def capture_pane_contents(target=None, opts=None):
|
||||
args = [ 'capture-pane', '-p' ]
|
||||
if opts:
|
||||
|
|
@ -247,7 +266,7 @@ def get_pane_info(target=None, capture=False, capturej=False):
|
|||
'window_id_full': r[0] + ':' + r[1],
|
||||
'pane_id': r[2],
|
||||
'pane_id_full': r[0] + ':' + r[1] + '.' + r[2],
|
||||
'pane_size': (int(r[3]), int(r[4])),
|
||||
'pane_size': (int(r[3]), int(r[4])), # (width, height)
|
||||
'zoomed': bool(int(r[5])),
|
||||
'cursor': copycursorpos if mode == 'copy-mode' else cursorpos,
|
||||
'scroll_position': int(r[11]) if r[11] != '' else None,
|
||||
|
|
@ -257,8 +276,10 @@ def get_pane_info(target=None, capture=False, capturej=False):
|
|||
if mode == 'copy-mode' and rdict['scroll_position'] != None and rdict['scroll_position'] > 0:
|
||||
capture_opts += [ '-S', str(-rdict['scroll_position']), '-E', str(-rdict['scroll_position'] + rdict['pane_size'][1] - 1) ]
|
||||
if capture:
|
||||
# The "normal" pane capture includes "hard" newlines at line wraps and truncates trailing spaces
|
||||
rdict['contents'] = capture_pane_contents(rdict['pane_id_full'], capture_opts)
|
||||
if capturej:
|
||||
# The "-J" pane capture includes trailing spaces and does not have newlines for wrapping
|
||||
rdict['contentsj'] = capture_pane_contents(rdict['pane_id_full'], [ '-J' ] + capture_opts)
|
||||
return rdict
|
||||
|
||||
|
|
@ -359,6 +380,17 @@ def gen_em_labels(n, chars=None, min_nchars=1, max_nchars=None):
|
|||
yield ''.join(label)
|
||||
|
||||
def process_pane_capture_lines(data, nlines=None):
|
||||
"""Given the string blob of data from `tmux capture-pane`, returns an array of line strings.
|
||||
|
||||
Arguments:
|
||||
data -- String blob of data from `tmux capture-pane`
|
||||
nlines -- Maximum number of lines to return
|
||||
|
||||
Returns:
|
||||
An array of line strings. Each line string corresponds to a single visible line such that
|
||||
a wrapped line is returned as multiple visible lines. Nonprintable characters are removed
|
||||
and tabs are converted to spaces.
|
||||
"""
|
||||
# processes pane capture data into an array of lines
|
||||
# also handles nonprintables
|
||||
lines = [
|
||||
|
|
@ -521,6 +553,7 @@ class PaneJumpAction:
|
|||
|
||||
# Fetch options
|
||||
self.em_label_chars = get_tmux_option('@copytk-label-chars', 'asdghklqwertyuiopzxcvbnmfj;')
|
||||
self.has_capital_label_chars = bool(re.search(r'[A-Z]', self.em_label_chars))
|
||||
|
||||
# Sanitize the J capture data by removing trailing spaces on each line
|
||||
self.copy_data = '\n'.join(( line.rstrip() for line in self.orig_pane['contentsj'].split('\n') ))
|
||||
|
|
@ -546,10 +579,13 @@ class PaneJumpAction:
|
|||
curses.curs_set(False)
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
curses.init_pair(1, curses.COLOR_RED, -1) # color for label first char
|
||||
curses.init_pair(2, curses.COLOR_YELLOW, -1) # color for label second+ char
|
||||
curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_YELLOW) # color for highlight
|
||||
curses.init_pair(4, curses.COLOR_RED, -1)
|
||||
def init_color(index, optname, default_fg, default_bg):
|
||||
pair = get_tmux_option_color_pair_curses(optname, default_fg, default_bg)
|
||||
curses.init_pair(index, pair[0], pair[1])
|
||||
init_color(1, '@copytk-color-labelchar', curses.COLOR_RED, -1) # first label char
|
||||
init_color(2, '@copytk-color-labelchar2', curses.COLOR_YELLOW, -1) # second+ label char
|
||||
init_color(3, '@copytk-color-highlight', curses.COLOR_GREEN, curses.COLOR_YELLOW) # highlight
|
||||
init_color(4, '@copytk-color-message', curses.COLOR_RED, -1) # status message
|
||||
self.stdscr.clear()
|
||||
|
||||
# Track the size as known by curses
|
||||
|
|
@ -706,6 +742,7 @@ class EasyMotionAction(PaneJumpAction):
|
|||
self.search_len = search_len
|
||||
self.case_sensitive_search = get_tmux_option('@copytk-case-sensitive-search', 'upper') # value values: on, off, upper
|
||||
self.min_match_spacing = int(get_tmux_option('@copytk-min-match-spacing', '2'))
|
||||
self.loc_label_mapping = {} # override mapping from match loc tuples to labels
|
||||
|
||||
def _em_filter_locs(self, locs):
|
||||
d = args.search_direction
|
||||
|
|
@ -755,6 +792,15 @@ class EasyMotionAction(PaneJumpAction):
|
|||
return search_str
|
||||
|
||||
def get_locations(self, action):
|
||||
"""Returns a list of (x, y) locations of potential match locations.
|
||||
|
||||
Arguments:
|
||||
action -- Either 'search' (to input a search char) or 'lines' (to use each line as a location)
|
||||
|
||||
Returns:
|
||||
A list of (x, y) tuples where y is the line number and x is the character in the line. The lines
|
||||
represent "physical" lines (eg. a single wrapped line is treated as multiple physical lines here.)
|
||||
"""
|
||||
pane_search_lines = self.display_content_lines
|
||||
log('\n'.join(pane_search_lines), 'pane_search_lines')
|
||||
|
||||
|
|
@ -771,7 +817,21 @@ class EasyMotionAction(PaneJumpAction):
|
|||
else:
|
||||
raise Exception('Invalid copytk easymotion action')
|
||||
|
||||
def do_easymotion(self, action, filter_locs=None, sort_close_to=None):
|
||||
def _input_easymotion_keys(self):
|
||||
"""Waits for easymotion keypresses to select match; filters possible matches as is executed."""
|
||||
# Wait for label presses
|
||||
keyed_label = ''
|
||||
while True: # loop over each key/char in the label
|
||||
keyed_label += self.getkey()
|
||||
self.cur_label_pos += 1
|
||||
self.match_locations = [ m for m in self.match_locations if m[2].startswith(keyed_label) ]
|
||||
if len(self.match_locations) < 2:
|
||||
break
|
||||
self.redraw()
|
||||
log('keyed label: ' + keyed_label, time=True)
|
||||
|
||||
|
||||
def do_easymotion(self, action, filter_locs=None, sort_close_to=None, save_labels=False):
|
||||
# Get possible jump locations sorted by proximity to cursor
|
||||
locs = self.get_locations(action)
|
||||
locs = self._em_filter_locs(locs)
|
||||
|
|
@ -783,21 +843,28 @@ class EasyMotionAction(PaneJumpAction):
|
|||
|
||||
# Assign each match a label
|
||||
label_it = gen_em_labels(len(locs), self.em_label_chars)
|
||||
self.match_locations = [ (ml[0], ml[1], next(label_it) ) for ml in locs ]
|
||||
self.match_locations = []
|
||||
used_labels = { label : loc for loc, label in self.loc_label_mapping.items() }
|
||||
for ml in locs:
|
||||
if ml in self.loc_label_mapping:
|
||||
label = self.loc_label_mapping[ml]
|
||||
else:
|
||||
while True:
|
||||
label = next(label_it)
|
||||
if label not in used_labels:
|
||||
break
|
||||
used_labels[label] = ml
|
||||
self.match_locations.append(( ml[0], ml[1], label ))
|
||||
|
||||
# If save_labels is true, preserve labels for locations across batches
|
||||
if save_labels:
|
||||
self.loc_label_mapping.update({ loc : label for label, loc in used_labels.items() })
|
||||
|
||||
# Draw labels
|
||||
self.redraw()
|
||||
|
||||
# Wait for label presses
|
||||
keyed_label = ''
|
||||
while True: # loop over each key/char in the label
|
||||
keyed_label += self.getkey()
|
||||
self.cur_label_pos += 1
|
||||
self.match_locations = [ m for m in self.match_locations if m[2].startswith(keyed_label) ]
|
||||
if len(self.match_locations) < 2:
|
||||
break
|
||||
self.redraw()
|
||||
log('keyed label: ' + keyed_label, time=True)
|
||||
# Wait for keypresses
|
||||
self._input_easymotion_keys()
|
||||
|
||||
if len(self.match_locations) == 0:
|
||||
return None
|
||||
|
|
@ -850,6 +917,76 @@ class EasyCopyAction(EasyMotionAction):
|
|||
self.flash_highlight_range((pos1, pos2))
|
||||
|
||||
|
||||
class LineCopyAction(EasyMotionAction):
|
||||
|
||||
def __init__(self, stdscr):
|
||||
super().__init__(stdscr)
|
||||
|
||||
# override from EasyMotionAction to support single-char line selection
|
||||
def _input_easymotion_keys(self):
|
||||
"""Waits for easymotion keypresses to select match; filters possible matches as is executed."""
|
||||
keyed_label = ''
|
||||
while True: # loop over each key/char in the label
|
||||
k = self.getkey()
|
||||
if not self.has_capital_label_chars and re.fullmatch('[A-Z]', k) and self.easymotion_phase == 0:
|
||||
# Enable single-line-copy mode
|
||||
k = k.lower()
|
||||
self.single_line_copy = True
|
||||
keyed_label += k
|
||||
self.cur_label_pos += 1
|
||||
self.match_locations = [ m for m in self.match_locations if m[2].startswith(keyed_label) ]
|
||||
if len(self.match_locations) < 2:
|
||||
break
|
||||
self.redraw()
|
||||
log('keyed label: ' + keyed_label, time=True)
|
||||
|
||||
def run(self):
|
||||
log('easycopy linecopy swapping in hidden pane', time=True)
|
||||
swap_hidden_pane(True)
|
||||
|
||||
# Input searches to get bounds
|
||||
self.easymotion_phase = 0
|
||||
self.single_line_copy = False
|
||||
pos1 = self.do_easymotion('lines', save_labels=True)
|
||||
if not pos1: return
|
||||
|
||||
if self.single_line_copy:
|
||||
# Copy single logical line starting at pos1.
|
||||
# Find end of line as a copy data index
|
||||
startidx = self.disp_copy_map[pos1]
|
||||
endidx = self.copy_data.find('\n', startidx)
|
||||
if endidx == -1:
|
||||
endidx = len(self.copy_data)
|
||||
pos2 = self.copy_disp_map[len(self.copy_data) - 1]
|
||||
selected_data = self.copy_data[startidx:]
|
||||
else:
|
||||
pos2 = self.copy_disp_map[endidx-1]
|
||||
selected_data = self.copy_data[startidx:endidx]
|
||||
else:
|
||||
self.highlight_ranges = [ (pos1, pos1) ]
|
||||
self.reset(keep_highlight=True)
|
||||
# restrict second search to after first position
|
||||
self.easymotion_phase = 1
|
||||
pos2 = self.do_easymotion(
|
||||
'lines',
|
||||
filter_locs=lambda loc: loc[1] > pos1[1] or (loc[1] == pos1[1] and loc[0] >= pos1[0]),
|
||||
sort_close_to=pos1
|
||||
)
|
||||
if not pos2: return
|
||||
|
||||
# since typing last n letters of word, advance end position by n-1 (-1 because range is inclusive)
|
||||
pos2 = (self.orig_pane['pane_size'][0] - 1, pos2[1])
|
||||
|
||||
# Find the data associated with this range and run the copy command
|
||||
selected_data = self.copy_data[self.disp_copy_map[pos1] : self.disp_copy_map[pos2] + 1]
|
||||
|
||||
log('Copied: ' + selected_data)
|
||||
execute_copy(selected_data)
|
||||
|
||||
# Flash selected range as confirmation
|
||||
self.flash_highlight_range((pos1, pos2))
|
||||
|
||||
|
||||
class QuickCopyAction(PaneJumpAction):
|
||||
|
||||
def __init__(self, stdscr, options_prefix='@copytk-quickcopy-'):
|
||||
|
|
@ -1140,6 +1277,9 @@ def run_easycopy(stdscr):
|
|||
nkeys = int(args.search_nkeys)
|
||||
EasyCopyAction(stdscr, nkeys).run()
|
||||
|
||||
def run_linecopy(stdscr):
|
||||
LineCopyAction(stdscr).run()
|
||||
|
||||
def run_quickcopy(stdscr):
|
||||
QuickCopyAction(stdscr).run()
|
||||
|
||||
|
|
@ -1232,6 +1372,8 @@ try:
|
|||
curses.wrapper(run_easymotion)
|
||||
elif args.action == 'easycopy':
|
||||
curses.wrapper(run_easycopy)
|
||||
elif args.action == 'linecopy':
|
||||
curses.wrapper(run_linecopy)
|
||||
elif args.action == 'quickcopy':
|
||||
curses.wrapper(run_quickcopy)
|
||||
elif args.action == 'quickopen':
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ tmux bind-key -T copytk Y run-shell -b "python3 $CURRENT_DIR/copytk.py easycopy
|
|||
tmux bind-key -T prefix S run-shell -b "python3 $CURRENT_DIR/copytk.py easycopy --search-nkeys 1"
|
||||
tmux bind-key -T prefix C-s run-shell -b "python3 $CURRENT_DIR/copytk.py easycopy --search-nkeys 1"
|
||||
|
||||
# tmux prefix: linecopy action bindings
|
||||
tmux bind-key -T prefix W run-shell -b "python3 $CURRENT_DIR/copytk.py linecopy"
|
||||
tmux bind-key -T prefix C-w run-shell -b "python3 $CURRENT_DIR/copytk.py linecopy"
|
||||
|
||||
# tmux prefix: quickcopy action bindings
|
||||
tmux bind-key -T prefix Q run-shell -b "python3 $CURRENT_DIR/copytk.py quickcopy"
|
||||
tmux bind-key -T prefix C-q run-shell -b "python3 $CURRENT_DIR/copytk.py quickcopy"
|
||||
|
|
|
|||
Loading…
Reference in a new issue