From 5c0ea04800987425aa9470e42ba8d4ad797e293e Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 30 May 2022 06:29:08 +0100 Subject: [PATCH 01/20] Implement cell deletion --- tabview/tabview.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tabview/tabview.py b/tabview/tabview.py index 4ed3c3d..c5c865c 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -318,6 +318,11 @@ def show_cell(self): TextBox(self.scr, data=s, title=self.location_string(yp, xp))() self.resize() + def delete_cell(self): + yp = self.y + self.win_y + xp = self.x + self.win_x + self.data[yp][xp] = '' + def show_info(self): """Display data information in a pop-up window @@ -689,6 +694,7 @@ def define_keys(self): '}': self.skip_to_col_change, '{': self.skip_to_col_change_reverse, '?': self.help, + 'd': self.delete_cell, curses.KEY_F1: self.help, curses.KEY_UP: self.up, curses.KEY_DOWN: self.down, From b47215cb80655f0214d4bc3d6db0be8178506da9 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 30 May 2022 06:30:36 +0100 Subject: [PATCH 02/20] Map DELETE key as delete_cell --- README.rst | 2 +- tabview/tabview.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 348ae67..8af2008 100644 --- a/README.rst +++ b/README.rst @@ -128,7 +128,7 @@ Keybindings: if num not given **Ctrl-g** Show file/data information **Insert or m** Memorize this position -**Delete or '** Return to memorized position (if any) +'** Return to memorized position (if any) **Enter** View full cell contents in pop-up window. **/** Search **n** Next search result diff --git a/tabview/tabview.py b/tabview/tabview.py index c5c865c..b4a0bb9 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -705,8 +705,8 @@ def define_keys(self): curses.KEY_PPAGE: self.page_up, curses.KEY_NPAGE: self.page_down, curses.KEY_IC: self.mark, - curses.KEY_DC: self.goto_mark, curses.KEY_ENTER: self.show_cell, + curses.KEY_DC: self.delete_cell, KEY_CTRL('a'): self.line_home, KEY_CTRL('e'): self.line_end, KEY_CTRL('l'): self.scr.redrawwin, From dcfe6f0c3a215269bb944e6a5618f7df9425d7eb Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 30 May 2022 10:53:23 +0100 Subject: [PATCH 03/20] Add header searching --- tabview/tabview.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index b4a0bb9..51b9f72 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -418,7 +418,8 @@ def search_results(self, rev=False, look_in_cur=False): else: # Skip back to the top if at the end of the data yp = xp = 0 - search_order = [self._search_cur_line_r, + search_order = [self._search_header, + self._search_cur_line_r, self._search_next_line_to_end, self._search_next_line_from_beg, self._search_cur_line_l] @@ -447,6 +448,16 @@ def _reverse_data(self, data, yp, xp): data[idx] = i return data, yp, xp + def _search_header(self, data, yp, xp): + """ Headers line first, from yp,xp to the right """ + res = False + for x, item in enumerate(self.header): + if self.search_str in item.lower(): + xp = x + res = True + break + return yp, xp, res + def _search_cur_line_r(self, data, yp, xp): """ Current line first, from yp,xp to the right """ res = False From 9d2ff5d936e067d49e8cba62349c796cb6dfd946 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 30 May 2022 11:31:08 +0100 Subject: [PATCH 04/20] Implement editing --- tabview/tabview.py | 47 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index 51b9f72..e5ed4b2 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -312,9 +312,6 @@ def show_cell(self): yp = self.y + self.win_y xp = self.x + self.win_x s = "\n" + self.data[yp][xp] - if not s: - # Only display pop-up if cells have contents - return TextBox(self.scr, data=s, title=self.location_string(yp, xp))() self.resize() @@ -323,6 +320,48 @@ def delete_cell(self): xp = self.x + self.win_x self.data[yp][xp] = '' + def _edit_validator(self, ch): + """Fix Enter and backspace for textbox. + + Used as an aux function for the textpad.edit method + + """ + if ch == curses.ascii.NL: # Enter + return curses.ascii.BEL + return ch + + def edit_cell(self): + yp = self.y + self.win_y + xp = self.x + self.win_x + self.box_height = self.max_y - int(self.max_y / 2) + box_height = int(self.box_height / 4) + + prompt = "Edit: " + scr2 = curses.newwin(box_height+1, self.max_x, self.max_y-box_height-1, 0) + scr3 = scr2.derwin(1, self.max_x-len(prompt)-3, 1, len(prompt)+1) + + scr2.box() + scr2.move(1, 1) + + addstr(scr2, prompt) + addstr(scr3, self.data[yp][xp]) + + scr2.refresh() + curses.curs_set(1) + + textpad = Textbox(scr3, insert_mode=True) + + self.data[yp][xp] = textpad.edit(self._edit_validator)[:-1] + + try: + curses.curs_set(0) + except _curses.error: + pass + + def duplicate_row(self): + yp = self.y + self.win_y + self.data.insert(yp+1, self.data[yp].copy()) + def show_info(self): """Display data information in a pop-up window @@ -706,6 +745,8 @@ def define_keys(self): '{': self.skip_to_col_change_reverse, '?': self.help, 'd': self.delete_cell, + 'e': self.edit_cell, + 'D': self.duplicate_row, curses.KEY_F1: self.help, curses.KEY_UP: self.up, curses.KEY_DOWN: self.down, From 60c4fb5568495968d3047a9e7073cebf79ab4d95 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 30 May 2022 12:22:41 +0100 Subject: [PATCH 05/20] Basic undo/redo --- tabview/tabview.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index e5ed4b2..0f15b68 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -105,6 +105,8 @@ def __init__(self, *args, **kwargs): self.max_y, self.max_x = 0, 0 self.num_columns = 0 self.vis_columns = 0 + self.undo_buffer = [] + self.redo_buffer = [] self.init_search = self.search_str = kwargs.get('search_str') self._search_win_open = 0 self.modifier = str() @@ -318,6 +320,10 @@ def show_cell(self): def delete_cell(self): yp = self.y + self.win_y xp = self.x + self.win_x + + undo_op = (yp, xp, self.data[yp][xp]) + if not self.undo_buffer or self.undo_buffer[0] != undo_op: + self.undo_buffer.insert(0, undo_op) self.data[yp][xp] = '' def _edit_validator(self, ch): @@ -351,6 +357,7 @@ def edit_cell(self): textpad = Textbox(scr3, insert_mode=True) + self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) self.data[yp][xp] = textpad.edit(self._edit_validator)[:-1] try: @@ -358,6 +365,20 @@ def edit_cell(self): except _curses.error: pass + def undo_redo(self, undo=True): + if undo: + from_buffer = self.undo_buffer + to_buffer = self.redo_buffer + else: + from_buffer = self.redo_buffer + to_buffer = self.undo_buffer + + if len(from_buffer): + yp, xp, value = from_buffer.pop(0) + to_buffer.insert(0, (yp, xp, self.data[yp][xp])) + + self.data[yp][xp] = value + def duplicate_row(self): yp = self.y + self.win_y self.data.insert(yp+1, self.data[yp].copy()) @@ -736,7 +757,7 @@ def define_keys(self): 's': self.sort_by_column, 'S': self.sort_by_column_reverse, 'y': self.yank_cell, - 'r': self.reload, + 'R': self.reload, 'c': self.toggle_column_width, 'C': self.set_current_column_width, ']': self.skip_to_row_change, @@ -747,6 +768,8 @@ def define_keys(self): 'd': self.delete_cell, 'e': self.edit_cell, 'D': self.duplicate_row, + 'u': self.undo_redo, + 'r': lambda: self.undo_redo(False), curses.KEY_F1: self.help, curses.KEY_UP: self.up, curses.KEY_DOWN: self.down, From aec1b9b415c8884e415e751d7fdedac4efe2fed5 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 30 May 2022 16:19:20 +0100 Subject: [PATCH 06/20] Implement saving --- tabview/tabview.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index 0f15b68..bb465ce 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -74,6 +74,7 @@ def __init__(self, *args, **kwargs): # http://bugs.python.org/issue2675 os.unsetenv('LINES') os.unsetenv('COLUMNS') + self.filename = kwargs.get('filename') self.scr = args[0] self.data = [[str(j) for j in i] for i in args[1]] self.info = kwargs.get('info') @@ -786,8 +787,16 @@ def define_keys(self): KEY_CTRL('e'): self.line_end, KEY_CTRL('l'): self.scr.redrawwin, KEY_CTRL('g'): self.show_info, + KEY_CTRL('s'): self.save, } + def save(self): + with open(self.filename, 'w') as csvfile: + writer = csv.writer(csvfile, quoting=csv.QUOTE_MINIMAL) + writer.writerow(self.header) + for row in self.data: + writer.writerow(row) + def run(self): # Clear the screen and display the menu of keys # Main loop: @@ -1408,7 +1417,8 @@ def view(data, enc=None, start_pos=(0, 0), column_width=20, column_gap=2, column_widths=column_widths, search_str=search_str, double_width=double_width, - info=info) + info=info, + filename=data) except (QuitException, KeyboardInterrupt): return 0 except ReloadException as e: From 79cd3f1177402ea750d6c194ea5b7bacbd56706b Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 30 May 2022 18:13:10 +0100 Subject: [PATCH 07/20] Save-on-exit and strip input --- tabview/tabview.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index bb465ce..19edca7 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -164,7 +164,9 @@ def column_xw(self, x): w = max(0, min(self.max_x - xp, self.column_width[self.win_x + x])) return xp, w - def quit(self): + def quit(self, save=False): + if save: + self.save() raise QuitException def reload(self): @@ -359,7 +361,7 @@ def edit_cell(self): textpad = Textbox(scr3, insert_mode=True) self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) - self.data[yp][xp] = textpad.edit(self._edit_validator)[:-1] + self.data[yp][xp] = textpad.edit(self._edit_validator)[:-1].strip() try: curses.curs_set(0) @@ -733,7 +735,7 @@ def define_keys(self): "'": self.goto_mark, 'L': self.page_right, 'H': self.page_left, - 'q': self.quit, + 'q': lambda: self.quit(True), 'Q': self.quit, '$': self.line_end, '^': self.line_home, From 1774338ee3809f9190b29c7527525608416315ef Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 30 May 2022 18:53:17 +0100 Subject: [PATCH 08/20] Implement HOME/END keys --- tabview/tabview.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index 19edca7..5264749 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -337,6 +337,10 @@ def _edit_validator(self, ch): """ if ch == curses.ascii.NL: # Enter return curses.ascii.BEL + elif ch == curses.KEY_HOME: + return self.textpad.do_command(KEY_CTRL('a')) + elif ch == curses.KEY_END: + return self.textpad.do_command(KEY_CTRL('e')) return ch def edit_cell(self): @@ -358,10 +362,10 @@ def edit_cell(self): scr2.refresh() curses.curs_set(1) - textpad = Textbox(scr3, insert_mode=True) + self.textpad = Textbox(scr3, insert_mode=True) self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) - self.data[yp][xp] = textpad.edit(self._edit_validator)[:-1].strip() + self.data[yp][xp] = self.textpad.edit(self._edit_validator)[:-1].strip() try: curses.curs_set(0) From 034d8c053117573a2e888aef099795f64dc66662 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Tue, 31 May 2022 09:20:40 +0100 Subject: [PATCH 09/20] Add ESC key to cancel cell edit --- tabview/tabview.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index 5264749..3816786 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -341,6 +341,9 @@ def _edit_validator(self, ch): return self.textpad.do_command(KEY_CTRL('a')) elif ch == curses.KEY_END: return self.textpad.do_command(KEY_CTRL('e')) + elif ch == curses.ascii.ESC: + self.textpad.insert_mode = False + return curses.ascii.BEL return ch def edit_cell(self): @@ -364,8 +367,10 @@ def edit_cell(self): self.textpad = Textbox(scr3, insert_mode=True) - self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) - self.data[yp][xp] = self.textpad.edit(self._edit_validator)[:-1].strip() + result = self.textpad.edit(self._edit_validator)[:-1].strip() + if self.textpad.insert_mode: # False if escape pressed (discard) + self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) + self.data[yp][xp] = result try: curses.curs_set(0) @@ -1415,6 +1420,9 @@ def view(data, enc=None, start_pos=(0, 0), column_width=20, column_gap=2, # cannot read the file return 1 + # https://stackoverflow.com/a/28020568 + os.environ.setdefault('ESCDELAY', '50') + curses.wrapper(main, buf, start_pos=start_pos, column_width=column_width, From 3a730b95877ec2fc3f74d80e9f86653fb2cbb591 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Tue, 31 May 2022 09:38:03 +0100 Subject: [PATCH 10/20] Show filename in terminal title --- tabview/tabview.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index 3816786..b174e85 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -110,6 +110,7 @@ def __init__(self, *args, **kwargs): self.redo_buffer = [] self.init_search = self.search_str = kwargs.get('search_str') self._search_win_open = 0 + self.modified = False self.modifier = str() self.define_keys() self.resize() @@ -123,6 +124,12 @@ def __init__(self, *args, **kwargs): self.goto_x(kwargs.get('start_pos')[1]) except (IndexError, TypeError): pass + self.set_term_title() + + def set_term_title(self, modified=False): + # https://stackoverflow.com/a/47262154 + print(f"\x1b]2;{self.filename}{'*'if modified else''}\x07", end='', flush=True) + self.modified = modified def _is_num(self, cell): try: @@ -324,10 +331,12 @@ def delete_cell(self): yp = self.y + self.win_y xp = self.x + self.win_x - undo_op = (yp, xp, self.data[yp][xp]) - if not self.undo_buffer or self.undo_buffer[0] != undo_op: - self.undo_buffer.insert(0, undo_op) - self.data[yp][xp] = '' + if self.data[yp][xp]: + undo_op = (yp, xp, self.data[yp][xp]) + if not self.undo_buffer or self.undo_buffer[0] != undo_op: + self.undo_buffer.insert(0, undo_op) + self.data[yp][xp] = '' + self.set_term_title(True) def _edit_validator(self, ch): """Fix Enter and backspace for textbox. @@ -371,6 +380,7 @@ def edit_cell(self): if self.textpad.insert_mode: # False if escape pressed (discard) self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) self.data[yp][xp] = result + self.set_term_title(True) try: curses.curs_set(0) @@ -390,10 +400,12 @@ def undo_redo(self, undo=True): to_buffer.insert(0, (yp, xp, self.data[yp][xp])) self.data[yp][xp] = value + self.set_term_title(True) def duplicate_row(self): yp = self.y + self.win_y self.data.insert(yp+1, self.data[yp].copy()) + self.set_term_title(True) def show_info(self): """Display data information in a pop-up window @@ -802,11 +814,13 @@ def define_keys(self): } def save(self): - with open(self.filename, 'w') as csvfile: - writer = csv.writer(csvfile, quoting=csv.QUOTE_MINIMAL) - writer.writerow(self.header) - for row in self.data: - writer.writerow(row) + if self.modified: + with open(self.filename, 'w') as csvfile: + writer = csv.writer(csvfile, quoting=csv.QUOTE_MINIMAL) + writer.writerow(self.header) + for row in self.data: + writer.writerow(row) + self.set_term_title() def run(self): # Clear the screen and display the menu of keys From 6b3baa2dc76a7fe76d7c4c1403dc9d5aca192ed7 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Thu, 2 Jun 2022 21:14:39 +0100 Subject: [PATCH 11/20] Shift+e for editing with blank input --- tabview/tabview.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index b174e85..ba84dab 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -355,7 +355,7 @@ def _edit_validator(self, ch): return curses.ascii.BEL return ch - def edit_cell(self): + def edit_cell(self, edit_existing=True): yp = self.y + self.win_y xp = self.x + self.win_x self.box_height = self.max_y - int(self.max_y / 2) @@ -369,7 +369,8 @@ def edit_cell(self): scr2.move(1, 1) addstr(scr2, prompt) - addstr(scr3, self.data[yp][xp]) + if edit_existing: + addstr(scr3, self.data[yp][xp]) scr2.refresh() curses.curs_set(1) @@ -791,6 +792,7 @@ def define_keys(self): '?': self.help, 'd': self.delete_cell, 'e': self.edit_cell, + 'E': lambda: self.edit_cell(False), 'D': self.duplicate_row, 'u': self.undo_redo, 'r': lambda: self.undo_redo(False), From 29f09b7b9350fec55fed7c06884c785cf8a13e6e Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Fri, 3 Jun 2022 15:30:45 +0100 Subject: [PATCH 12/20] Row deletion and naive undo/redo --- tabview/tabview.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index ba84dab..f0831e7 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -338,6 +338,15 @@ def delete_cell(self): self.data[yp][xp] = '' self.set_term_title(True) + def delete_row(self): + yp = self.y + self.win_y + + undo_op = (yp, True, self.data.pop(yp)) + if not self.undo_buffer or self.undo_buffer[0] != undo_op: + self.undo_buffer.insert(0, undo_op) + + self.set_term_title(True) + def _edit_validator(self, ch): """Fix Enter and backspace for textbox. @@ -398,9 +407,19 @@ def undo_redo(self, undo=True): if len(from_buffer): yp, xp, value = from_buffer.pop(0) - to_buffer.insert(0, (yp, xp, self.data[yp][xp])) - - self.data[yp][xp] = value + # FIXME: Undo/redo for row deletion is unstable (deleting multiple rows) + if xp is True: + # Row deletion - undo by reinserting row + insert_index = min(yp, len(self.data)) + self.data.insert(insert_index, value) + to_buffer.insert(0, (insert_index, False, value)) + elif xp is False: + # Row insertion - undo by deleting row + to_buffer.insert(0, (yp, True, value)) + self.data.pop(yp) + else: + to_buffer.insert(0, (yp, xp, self.data[yp][xp])) + self.data[yp][xp] = value self.set_term_title(True) def duplicate_row(self): @@ -807,7 +826,7 @@ def define_keys(self): curses.KEY_NPAGE: self.page_down, curses.KEY_IC: self.mark, curses.KEY_ENTER: self.show_cell, - curses.KEY_DC: self.delete_cell, + curses.KEY_DC: self.delete_row, KEY_CTRL('a'): self.line_home, KEY_CTRL('e'): self.line_end, KEY_CTRL('l'): self.scr.redrawwin, From 46600140305254cb0ed15bf369674f10f799491f Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Fri, 3 Jun 2022 21:08:18 +0100 Subject: [PATCH 13/20] Add dialog for quitting with unsaved changes --- tabview/tabview.py | 59 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index f0831e7..2c005e4 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -44,6 +44,19 @@ def insstr(*args): return scr.insstr(*args) +# https://github.com/jacklam718/cursesDialog/blob/master/cursDialog.py#L188 +def rectangle(win, begin_y, begin_x, height, width, attr): + win.vline(begin_y, begin_x, curses.ACS_VLINE, height, attr) + win.hline(begin_y, begin_x, curses.ACS_HLINE, width, attr) + win.hline(height+begin_y, begin_x, curses.ACS_HLINE, width, attr) + win.vline(begin_y, begin_x+width, curses.ACS_VLINE, height, attr) + win.addch(begin_y, begin_x, curses.ACS_ULCORNER, attr) + win.addch(begin_y, begin_x+width, curses.ACS_URCORNER, attr) + win.addch(height+begin_y, begin_x, curses.ACS_LLCORNER, attr) + win.addch(begin_y+height, begin_x+width, curses.ACS_LRCORNER, attr) + win.refresh() + + class ReloadException(Exception): def __init__(self, start_pos, column_width, column_gap, column_widths, search_str): @@ -172,10 +185,52 @@ def column_xw(self, x): return xp, w def quit(self, save=False): - if save: - self.save() + if save and self.modified: + resp = self.ask_save() + if resp == 'Cancel': + # Prevent quitting + return + elif resp == 'Yes': + # Ask Y/N + self.save() raise QuitException + def ask_save(self): + focus = 0 + title = 'Save?' + options = ((4, ' Yes '), (25, ' No '), (46, 'Cancel')) # x-pos, text + y, x = (10, 56) # height, width + + win = curses.newwin(y, x, int((self.max_y/2)-y/2), int((self.max_x/2)-x/2)) + win.box() + win.keypad(1) + + curses.curs_set(0) + curses.noecho() + curses.cbreak() + + win.addstr(0, int(x/2-len(title)/2), title, curses.A_BOLD | curses.A_STANDOUT) + + # Draws button outlines + for x_pos, text in options: + rectangle(win, int(y/1.5), x_pos-1, 2, len(text)+1, curses.A_BOLD) + + while True: + # Draw button text and highlight selected button (rect) + for idx, option in enumerate(options): + style = curses.A_BOLD + if idx == focus: + style |= curses.A_STANDOUT + win.addstr(int(y/1.5)+1, option[0], option[1], style) + rectangle(win, int(y/1.5), option[0]-1, 2, len(option[1])+1, style) + + win.refresh() + key = win.getch() + if key in [curses.KEY_LEFT, curses.KEY_RIGHT]: + focus = max(0, focus-1) if key == curses.KEY_LEFT else min(2, focus+1) + elif key == ord('\n'): + return options[focus][1].strip() # Return stripped option text + def reload(self): start_pos = (self.y + self.win_y + 1, self.x + self.win_x + 1) raise ReloadException(start_pos, self.column_width_mode, From f7bbb13f7dc030655f856b2828a72d4e94e4cc6a Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Sat, 4 Jun 2022 17:38:04 +0100 Subject: [PATCH 14/20] Access y/n/Cancel using hotkeys --- tabview/tabview.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tabview/tabview.py b/tabview/tabview.py index 2c005e4..8631ff7 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -199,6 +199,7 @@ def ask_save(self): focus = 0 title = 'Save?' options = ((4, ' Yes '), (25, ' No '), (46, 'Cancel')) # x-pos, text + option_hotkeys = (ord('y'), ord('n'), curses.ascii.ESC) y, x = (10, 56) # height, width win = curses.newwin(y, x, int((self.max_y/2)-y/2), int((self.max_x/2)-x/2)) @@ -228,6 +229,8 @@ def ask_save(self): key = win.getch() if key in [curses.KEY_LEFT, curses.KEY_RIGHT]: focus = max(0, focus-1) if key == curses.KEY_LEFT else min(2, focus+1) + elif key in option_hotkeys: + focus = option_hotkeys.index(key) elif key == ord('\n'): return options[focus][1].strip() # Return stripped option text From e27021b1f03d035a0d5a1bbbec91f82671c997f0 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 6 Jun 2022 23:54:02 +0100 Subject: [PATCH 15/20] Use py_curses_editor for editing cells --- tabview/tabview.py | 49 ++++++++++++---------------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index 8631ff7..f0a45b8 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -5,6 +5,8 @@ Based on code contributed by A.M. Kuchling """ +from editor.editor import Editor + import csv import _curses import curses @@ -405,47 +407,22 @@ def delete_row(self): self.set_term_title(True) - def _edit_validator(self, ch): - """Fix Enter and backspace for textbox. - - Used as an aux function for the textpad.edit method - - """ - if ch == curses.ascii.NL: # Enter - return curses.ascii.BEL - elif ch == curses.KEY_HOME: - return self.textpad.do_command(KEY_CTRL('a')) - elif ch == curses.KEY_END: - return self.textpad.do_command(KEY_CTRL('e')) - elif ch == curses.ascii.ESC: - self.textpad.insert_mode = False - return curses.ascii.BEL - return ch - def edit_cell(self, edit_existing=True): yp = self.y + self.win_y xp = self.x + self.win_x - self.box_height = self.max_y - int(self.max_y / 2) - box_height = int(self.box_height / 4) + box_height = int((self.max_y - int(self.max_y / 2)) / 4) + data = self.data[yp][xp] if edit_existing else "" prompt = "Edit: " - scr2 = curses.newwin(box_height+1, self.max_x, self.max_y-box_height-1, 0) - scr3 = scr2.derwin(1, self.max_x-len(prompt)-3, 1, len(prompt)+1) - - scr2.box() - scr2.move(1, 1) - - addstr(scr2, prompt) - if edit_existing: - addstr(scr3, self.data[yp][xp]) - - scr2.refresh() - curses.curs_set(1) - - self.textpad = Textbox(scr3, insert_mode=True) - - result = self.textpad.edit(self._edit_validator)[:-1].strip() - if self.textpad.insert_mode: # False if escape pressed (discard) + editor = Editor( + self.scr, title=prompt, inittext=data, max_paragraphs=1, + win_size=(box_height+1, self.max_x), + win_location=(self.max_y-box_height-1, 0), + ) + editor.end() # Move to end of text for easier editing + + result = editor().strip() + if result != self.data[yp][xp]: self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) self.data[yp][xp] = result self.set_term_title(True) From 8970c2283eb955fb3ff5ea3cfd022ef2474de6c8 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Tue, 7 Jun 2022 00:24:19 +0100 Subject: [PATCH 16/20] Convert Help to py_curses_editor for page up/down functionality --- tabview/tabview.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index f0a45b8..4d07d6f 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -649,7 +649,12 @@ def help(self): idx = help_txt.index('Keybindings:\n') help_txt = [i.replace('**', '') for i in help_txt[idx:] if '===' not in i] - TextBox(self.scr, data="".join(help_txt), title="Help")() + box_height = self.max_y - int(self.max_y / 2) + Editor( + self.scr, title="Help", inittext="".join(help_txt), edit=False, + win_size=(box_height+1, self.max_x), + win_location=(self.max_y-box_height-1, 0), + )() self.resize() def toggle_header(self): From 3cc375a55b2ec93cf1fd2db02fd515b88d673d80 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Tue, 7 Jun 2022 00:38:56 +0100 Subject: [PATCH 17/20] Update README --- README.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 8af2008..eaa5ab9 100644 --- a/README.rst +++ b/README.rst @@ -33,6 +33,7 @@ Posted by Scott Hansen Other Contributors: + + Abdurrahmaan Iqbal + Matus Gura + Nathan Typanski + Sébastien Celles @@ -44,7 +45,7 @@ it are shown the contents of that cell. Features: --------- * Python 3.4+ -* Spreadsheet-like view for easily visualizing tabular data +* Spreadsheet-like view for easily visualizing and editing tabular data * Vim-like navigation (h,j,k,l, g(top), G(bottom), 12G goto line 12, m - mark, ' - goto mark, etc.) * Toggle persistent header row @@ -128,8 +129,14 @@ Keybindings: if num not given **Ctrl-g** Show file/data information **Insert or m** Memorize this position -'** Return to memorized position (if any) +**'** Return to memorized position (if any) **Enter** View full cell contents in pop-up window. +**d** (Editing) Delete cell content +**Delete** (Editing) Delete entire row +**e/E** (Editing) Edit cell - `e` edits current content, `E` does not. +**D** (Editing) Duplicate row +**u/r** (Editing) Undo/redo (NOTE: naïve implementation) +**CTRL+S** (Editing) Save file **/** Search **n** Next search result **p** Previous search result @@ -143,7 +150,7 @@ Keybindings: **A** 'Natural Sort' the table (descending) **#** Sort numerically by the current column (ascending) **@** Sort numerically by the current column (descending) -**r** Reload file/data. Also resets sort order +**R** Reload file/data. Also resets sort order **y** Yank cell contents to the clipboard (requires xsel or xclip) **[num]c** Toggle variable column width mode (mode/max), From 99ac5f172a2374a30610dae75f79691978defad1 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Fri, 10 Jun 2022 19:52:34 +0100 Subject: [PATCH 18/20] Don't overwrite cell if editor's quit_nosave was triggered --- tabview/tabview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabview/tabview.py b/tabview/tabview.py index 4d07d6f..9439f24 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -422,7 +422,7 @@ def edit_cell(self, edit_existing=True): editor.end() # Move to end of text for easier editing result = editor().strip() - if result != self.data[yp][xp]: + if editor.edit and result != self.data[yp][xp]: self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) self.data[yp][xp] = result self.set_term_title(True) From 3bcc769661cce588b2fe2bb006360f6b10cc1149 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Fri, 10 Jun 2022 20:03:02 +0100 Subject: [PATCH 19/20] Paste yanked value --- README.rst | 2 +- tabview/tabview.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index eaa5ab9..b570a1e 100644 --- a/README.rst +++ b/README.rst @@ -151,7 +151,7 @@ Keybindings: **#** Sort numerically by the current column (ascending) **@** Sort numerically by the current column (descending) **R** Reload file/data. Also resets sort order -**y** Yank cell contents to the clipboard +**y/P** Yank cell contents to the clipboard (P pastes last yank) (requires xsel or xclip) **[num]c** Toggle variable column width mode (mode/max), or set width to [num] diff --git a/tabview/tabview.py b/tabview/tabview.py index 9439f24..1d83297 100644 --- a/tabview/tabview.py +++ b/tabview/tabview.py @@ -127,6 +127,7 @@ def __init__(self, *args, **kwargs): self._search_win_open = 0 self.modified = False self.modifier = str() + self.yank_buffer = None self.define_keys() self.resize() self.display() @@ -432,6 +433,14 @@ def edit_cell(self, edit_existing=True): except _curses.error: pass + def paste_cell(self): + yp = self.y + self.win_y + xp = self.x + self.win_x + if self.yank_buffer: + self.undo_buffer.insert(0, (yp, xp, self.data[yp][xp])) + self.data[yp][xp] = self.yank_buffer + self.set_term_title(True) + def undo_redo(self, undo=True): if undo: from_buffer = self.undo_buffer @@ -791,7 +800,7 @@ def set_current_column_width(self): def yank_cell(self): yp = self.y + self.win_y xp = self.x + self.win_x - s = self.data[yp][xp] + self.yank_buffer = self.data[yp][xp] # Bail out if not running in X try: os.environ['DISPLAY'] @@ -801,7 +810,7 @@ def yank_cell(self): ['xsel', '-i']): try: Popen(cmd, stdin=PIPE, - universal_newlines=True).communicate(input=s) + universal_newlines=True).communicate(input=self.yank_buffer) except IOError: pass @@ -841,6 +850,7 @@ def define_keys(self): 's': self.sort_by_column, 'S': self.sort_by_column_reverse, 'y': self.yank_cell, + 'P': self.paste_cell, 'R': self.reload, 'c': self.toggle_column_width, 'C': self.set_current_column_width, From 9597648d0dd6e6fb18e07229d29a3460d2ea8fba Mon Sep 17 00:00:00 2001 From: Scott Hansen Date: Sun, 18 Sep 2022 15:48:46 -0700 Subject: [PATCH 20/20] Update email address --- LICENSE.txt | 2 +- README.rst | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 047fe8a..1ab7f6b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Scott Hansen +Scott Hansen Based on code contributed by A.M. Kuchling diff --git a/README.rst b/README.rst index b570a1e..160dde3 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ limited. For a more fully featured CSV viewer/spreadsheet app check out the** View a CSV file in a spreadsheet-like display. -Posted by Scott Hansen +Posted by Scott Hansen Original code forked from: http://www.amk.ca/files/simple/tabview.txt diff --git a/setup.py b/setup.py index 0dc62ac..7dc1d5f 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ description="A curses command-line CSV and list (tabular data) viewer", long_description=open('README.rst', 'rb').read().decode('utf-8'), author="Scott Hansen", - author_email="firecat4153@gmail.com", + author_email="tech@firecat53.net", url="https://github.com/Tabviewer/tabview", download_url="https://github.com/Tabviewer/tabview/tarball/1.4.4", packages=['tabview'],