diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb5e7e5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2021 Chris Breneman + +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. diff --git a/copytk.py b/copytk.py index 191ddcc..f5008af 100644 --- a/copytk.py +++ b/copytk.py @@ -1,3 +1,6 @@ +# Tmux Copy Toolkit +# (C) Chris Breneman 2021 + import os import os.path import sys @@ -13,7 +16,8 @@ from datetime import datetime import time import platform -logdir = '/tmp/tmplog' +#logdir = '/tmp/copytklog' +logdir = None python_command = 'python3' # Find full path to tmux command so it can be invoked without a shell @@ -40,17 +44,6 @@ match_expr_presets = { 'paths': r'(?:^|[][\s:=,#$"{}<>()'+"'"+r'])((?:(?:/|~/|[A-Z]:[\\/])(?:(?:(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])*(?:(?:(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6}|(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)[\\/]?)|(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)[\\/](?:(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)[\\/]?|(?:(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6})|(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)[\\/](?:(?:(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])+(?:(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)[\\/]?|(?:(?:[a-zA-Z0-9_-]{1,30}|\.|\.\.)|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6})))(?:$|[][\s:=,#$"{}<>()'+"'"+r'])', # Isolated filenames without paths 'filenames': r'(?:^|[][\s:=,#$"{}<>()/'+"'"+r'])([a-zA-Z0-9_-]{1,30}\.[a-zA-Z][a-zA-Z0-9]{0,5})(?:$|[][\s:=,#$"{}<>()'+"'"+r'])' - - # long regexes generated by _regex_builds.py - # matches things that look like common URLs. does not match esoteric 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'])', - # matches things that look like UNIX or Windows style absolute paths -# 'abspaths': r'(?:^|[][\s:=,#$"{}()'+"'"+r'])((?:/|~/|[A-Z]:[\\/])(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])*(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6}|[a-zA-Z0-9_-]{1,30}[\\/]?))(?:$|[][\s:=,#$"{}()'+"'"+r'])', - # matches both things that look like absolute paths, and relative paths with at least one separator - # may also match spaces in path components, but never at the beginning or end, and only in an inner component -# 'paths': r'(?:^|[][\s:=,#$"{}()'+"'"+r'])((?:(?:/|~/|[A-Z]:[\\/])(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])*(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6}|[a-zA-Z0-9_-]{1,30}[\\/]?)|[a-zA-Z0-9_-]{1,30}[\\/](?:[a-zA-Z0-9_-]{1,30}[\\/]?|(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6})|[a-zA-Z0-9_-]{1,30}[\\/](?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])+(?:[a-zA-Z0-9_-]{1,30}[\\/]?|(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6})))(?:$|[][\s:=,#$"{}()'+"'"+r'])', - # matches things that look like an isolated filename (no path) -# 'filenames': r'(?:^|[][\s:=,#$"{}()'+"'"+r'])([a-zA-Z0-9_-]{1,30}\.[a-zA-Z][a-zA-Z0-9]{0,5})(?:$|[][\s:=,#$"{}()'+"'"+r'])' } @@ -181,7 +174,6 @@ def fetch_tmux_options(optmode='g'): else: rval += c val = rval - log('Got option value: ' + val) opts[name] = val tmux_options_cache[optmode] = opts return opts @@ -533,9 +525,8 @@ class PaneJumpAction: aligninfo = align_capture_data(self.orig_pane['contents'], self.copy_data, self.orig_pane['pane_size']) if aligninfo == None: log('alignment failed') - # TODO: Remove exception, allow fallback - raise Exception('alignment failed') - # Fall back to just mapping the display data to itself + # raise Exception('alignment failed') + # Fall back to just mapping the display data to itself. Will break wrapped lines. self.copy_data = self.orig_pane['contents'] self.disp_copy_map = get_data_xy_idx_map(self.copy_data, self.orig_pane['pane_size']) self.copy_disp_map = get_data_xy_idx_rev_map(self.copy_data, self.orig_pane['pane_size']) @@ -609,7 +600,9 @@ class PaneJumpAction: else: self.stdscr.addstr(y, x, s, a) except Exception as err: - log(f'Error writing str to screen. curses_size={self.curses_size} linelen={len(line)} i={i} err={str(err)}') + pass + # note: errors are expected in writes to bottom-right + #log(f'Error writing str to screen. curses_size={self.curses_size} linelen={len(line)} i={i} err={str(err)}') def _redraw_contents(self): line_width = min(self.curses_size[1], self.orig_pane['pane_size'][0]) @@ -623,7 +616,6 @@ class PaneJumpAction: line_width = min(self.curses_size[1], self.orig_pane['pane_size'][0]) hlattr = curses.color_pair(3) for rng in self.highlight_ranges: - log('Drawing hl range: ' + str(rng)) for i in range(rng[0][1], rng[1][1] + 1): line = self.display_content_lines[i] if i < rng[0][1] or i > rng[1][1]: # whole line not hl @@ -638,7 +630,6 @@ class PaneJumpAction: self.addstr(i, 0, line[0:rng[1][0]+1], hlattr) else: assert(False) - log('Done drawing hl range') def _redraw_labels(self): line_width = min(self.curses_size[1], self.orig_pane['pane_size'][0]) @@ -650,12 +641,14 @@ class PaneJumpAction: try: self.stdscr.addstr(row, col, label[self.cur_label_pos], curses.color_pair(1)) except Exception as err: - log(f'Error writing str to screen. curses_size={self.curses_size} linelen={len(line)} i={i} err={str(err)}') + pass + #log(f'Error writing str to screen. curses_size={self.curses_size} linelen={len(line)} i={i} err={str(err)}') if len(label) > self.cur_label_pos + 1: try: self.stdscr.addstr(row, col+1, label[self.cur_label_pos+1:], curses.color_pair(2)) except Exception as err: - log(f'Error writing str to screen. curses_size={self.curses_size} linelen={len(line)} i={i} err={str(err)}') + pass + #log(f'Error writing str to screen. curses_size={self.curses_size} linelen={len(line)} i={i} err={str(err)}') def redraw(self): self._redraw_contents() @@ -764,7 +757,6 @@ class EasyMotionAction(PaneJumpAction): return search_str def get_locations(self, action): - #pane_search_lines = process_pane_capture_lines(self.orig_pane['contents'], self.orig_pane['pane_size'][1]) pane_search_lines = self.display_content_lines log('\n'.join(pane_search_lines), 'pane_search_lines') @@ -868,7 +860,6 @@ class QuickCopyAction(PaneJumpAction): self.options_prefix = options_prefix self._load_options(options_prefix) self.em_label_chars = ''.join(( c for c in self.em_label_chars if c not in self.next_batch_char )) - log('constructor finished') def _load_options(self, prefix='@copytk-quickcopy-'): # Load in the tiers of match expressions. @@ -877,7 +868,6 @@ class QuickCopyAction(PaneJumpAction): # The set of tiers is terminated by a missing 0 index for the tier. tier_exprs = [] # list (of tiers) of lists of strings tier_ctr = 0 - log('getting options') while True: l = get_tmux_option(prefix + 'match-' + str(tier_ctr), aslist=True, userlist=True) if l == None or len(l) == 0: @@ -964,8 +954,6 @@ class QuickCopyAction(PaneJumpAction): batches = [] log('start arrange_matches') while len(matches) > 0: # iterate over batches - #log('MATCHES:') - #log(str(matches)) last_added_tier = None overlaps = [] virt = [ False ] * len(self.copy_data) @@ -1060,13 +1048,11 @@ class QuickCopyAction(PaneJumpAction): log('quickcopy run') # Get a list of all matches matches = self.find_matches() - log('find_matches() result: ' + str(matches)) if len(matches) == 0: raise ActionCanceled() log('got matches') # Group them into display batches batches = self.arrange_matches(matches, self.pack_tiers) - log('arrange_matches() result: ' + str(batches)) log('arranged matches') swap_hidden_pane(True) @@ -1158,7 +1144,6 @@ def run_easycopy(stdscr): EasyCopyAction(stdscr, nkeys).run() def run_quickcopy(stdscr): - log('run_quickcopy') QuickCopyAction(stdscr).run() def run_quickopen(stdscr): diff --git a/copytk.tmux b/copytk.tmux index bc1a32c..21703e8 100755 --- a/copytk.tmux +++ b/copytk.tmux @@ -65,6 +65,7 @@ tmux set -g @copytk-quickcopy-match-3-2 ''\''([^'\'\\'n]*)'\' # Match whole lines tmux set -g @copytk-quickcopy-match-4-0 lines +# Matches for quickopen tmux set -g @copytk-quickopen-match-0-0 urls tmux set -g @copytk-quickopen-match-0-1 abspaths