diff --git a/src/class_cursor.py b/src/class_cursor.py index 782d918..2a26dbf 100644 --- a/src/class_cursor.py +++ b/src/class_cursor.py @@ -6,8 +6,10 @@ """ from enum import Enum -from functools import wraps -from typing import Any, Callable, Iterable, TypeVar + +# from functools import wraps +# from typing import Any, Callable, Iterable, TypeVar +from typing import Iterable, TypeVar from src.class_todo import Todos from src.get_args import GUI_TYPE, GuiType @@ -49,126 +51,118 @@ class Cursor: """ def __init__(self, position: int, todos: Todos) -> None: - self.positions: Positions = Positions([position]) - self.direction: _Direction = _Direction.NONE - self.todos: Todos = todos + self._positions: Positions = Positions([position]) + self._direction: _Direction = _Direction.NONE + # self._todos: Todos = todos + _ = todos def __len__(self) -> int: - return len(self.positions) + return len(self._positions) def __str__(self) -> str: - return str(self.positions[0]) + return str(self._positions[0]) def __repr__(self) -> str: - return " ".join(map(str, self.positions)) + return " ".join(map(str, self._positions)) def __int__(self) -> int: - return self.positions[0] + return self._positions[0] def __contains__(self, child: int) -> bool: - return child in self.positions + return child in self._positions def get(self) -> Positions: """Return a `Positions` object holding the current cursor""" - return self.positions + return self._positions def get_first(self) -> int: """Return the top-most selected position""" - return self.positions[0] + return self._positions[0] def get_last(self) -> int: """Return the bottom-most selected position""" - return self.positions[-1] - - @staticmethod - def _updates_cursor( - direction: _Direction = _Direction.NONE, - ) -> Callable[[Callable[..., T]], Callable[..., T]]: - """ - Decorate every function that updates the cursor. - This function ensures folded todos are handled - properly. This basically treats each folded group - of todos like its own individual todo. - """ - - def _decorator(func: Callable[..., T]) -> Callable[..., T]: - @wraps(func) - def _inner(self: "Cursor", *args: list[Any], **kwargs: dict[Any, Any]) -> T: - for pos in self.positions: - # if not self.todos[pos].is_folded_parent(): - # break - count = 0 - while True: - count += 1 - if not self.todos[pos + count].is_folded(): - break - if direction == _Direction.UP: - self.multiselect_up() - continue - if direction == _Direction.DOWN: - self.multiselect_down(len(self.todos)) - continue - if direction == _Direction.NONE: - func(self, *args, **kwargs) - return func(self, *args, **kwargs) - - return _inner - - return _decorator + return self._positions[-1] + + # @staticmethod + # def _updates_cursor( + # direction: _Direction = _Direction.NONE, + # ) -> Callable[[Callable[..., T]], Callable[..., T]]: + # """ + # Decorate every function that updates the cursor. + # This function ensures folded todos are handled + # properly. This basically treats each folded group + # of todos like its own individual todo. + # """ + + # def _decorator(func: Callable[..., T]) -> Callable[..., T]: + # @wraps(func) + # def _inner(self: "Cursor", *args: list[Any], **kwargs: dict[Any, Any]) -> T: + # for pos in self._positions: + # # if not self._todos[pos].is_folded_parent(): + # # break + # count = 0 + # while True: + # count += 1 + # if not self._todos[pos + count].is_folded(): + # break + # if direction == _Direction.UP: + # self.multiselect_up() + # continue + # if direction == _Direction.DOWN: + # self.multiselect_down(len(self._todos)) + # continue + # if direction == _Direction.NONE: + # func(self, *args, **kwargs) + # return func(self, *args, **kwargs) + + # return _inner + + # return _decorator def set_to(self, position: int) -> None: """Replace the entire cursor with a new single position""" - self.positions = Positions([position]) - - def set_to_passthrough(self, t_position: tuple[T, int]) -> T: - """ - Replace the entire cursor with a new single position - and pass the t portion of the tuple through - the method. - """ - self.set_to(t_position[1]) - return t_position[0] + self._positions = Positions([position]) def override_passthrough(self, passthrough: T, positions: Positions) -> T: """ Replace the cursor with a new `Positions`, and pass the passthrough through the method. """ - self.positions = positions + self._positions = positions return passthrough def single_up(self, max_len: int) -> None: """Move a cursor with length 1 up by 1""" - if len(self.positions) == max_len: + if len(self._positions) == max_len: self.set_to(0) return - if min(self.positions) == 0: + if min(self._positions) == 0: return - self.set_to(min(self.positions) - 1) + self.set_to(min(self._positions) - 1) # while self.todos[self.get_first()].is_folded(): # self.multiselect_up() def slide_up(self) -> None: """Shift each value in the cursor up by 1""" - if min(self.positions) == 0: + if min(self._positions) == 0: return - self.positions.insert(0, min(self.positions) - 1) - self.positions.pop() + self._positions.insert(0, min(self._positions) - 1) + self._positions.pop() def single_down(self, max_len: int) -> None: """Move a cursor with length 1 down by 1""" - if len(self.positions) == max_len: - self.set_to(min(self.positions)) - if max(self.positions) >= max_len - 1: + if len(self._positions) == max_len: + self.set_to(min(self._positions)) + if max(self._positions) >= max_len - 1: return - self.set_to(max(self.positions) + 1) + self.set_to(max(self._positions) + 1) def slide_down(self, max_len: int) -> None: """Shift each value in the cursor down by 1""" - if max(self.positions) >= max_len - 1: + if max(self._positions) >= max_len - 1: return - self.positions.append(max(self.positions) + 1) - self.positions.pop(0) + self._positions.append(max(self._positions) + 1) + self._positions.pop(0) def to_top(self) -> None: """Move the cursor to the top""" @@ -180,21 +174,21 @@ def to_bottom(self, len_list: int) -> None: def _select_next(self) -> None: """Extend the cursor down by 1""" - self.positions.append(max(self.positions) + 1) + self._positions.append(max(self._positions) + 1) def _deselect_next(self) -> None: """Retract the cursor by 1""" - if len(self.positions) > 1: - self.positions.remove(max(self.positions)) + if len(self._positions) > 1: + self._positions.remove(max(self._positions)) def _deselect_prev(self) -> None: """Remove the first position of the cursor""" - if len(self.positions) > 1: - self.positions.remove(min(self.positions)) + if len(self._positions) > 1: + self._positions.remove(min(self._positions)) def _select_prev(self) -> None: """Extend the cursor up by 1""" - self.positions.insert(0, min(self.positions) - 1) + self._positions.insert(0, min(self._positions) - 1) def get_deletable(self) -> Positions: """ @@ -202,25 +196,25 @@ def get_deletable(self) -> Positions: set to the minimum position of the current Cursor """ - return Positions([min(self.positions) for _ in self.positions]) + return Positions([min(self._positions) for _ in self._positions]) def multiselect_down(self, max_len: int) -> None: """Extend the cursor down by 1""" - if max(self.positions) >= max_len - 1: + if max(self._positions) >= max_len - 1: return - if len(self.positions) == 1 or self.direction == _Direction.DOWN: + if len(self._positions) == 1 or self._direction == _Direction.DOWN: self._select_next() - self.direction = _Direction.DOWN + self._direction = _Direction.DOWN return self._deselect_prev() def multiselect_up(self) -> None: """Extend the cursor up by 1""" - if min(self.positions) == 0 and self.direction == _Direction.UP: + if min(self._positions) == 0 and self._direction == _Direction.UP: return - if len(self.positions) == 1 or self.direction == _Direction.UP: + if len(self._positions) == 1 or self._direction == _Direction.UP: self._select_prev() - self.direction = _Direction.UP + self._direction = _Direction.UP return self._deselect_next() @@ -229,7 +223,7 @@ def multiselect_top(self) -> None: Select every position between 0 and the current top of the selection """ - for _ in range(self.positions[0], 0, -1): + for _ in range(self._positions[0], 0, -1): self.multiselect_up() def multiselect_bottom(self, max_len: int) -> None: @@ -238,13 +232,13 @@ def multiselect_bottom(self, max_len: int) -> None: current top of the selection and the `max_len` of the list """ - for _ in range(self.positions[0], max_len): + for _ in range(self._positions[0], max_len): self.multiselect_down(max_len) def _multiselect_to(self, position: int, max_len: int) -> None: """Select from current position up or down `position`""" - direction = -1 if position < self.positions[0] else 1 - for _ in range(self.positions[0], position, direction): + direction = -1 if position < self._positions[0] else 1 + for _ in range(self._positions[0], position, direction): if direction == 1: self.multiselect_down(max_len) continue @@ -283,9 +277,9 @@ def relative_to( key = stdscr.getch() # alt + ... stdscr.nodelay(False) if key == Key.k: - operation(self.positions[0] - int(total), max_len) + operation(self._positions[0] - int(total), max_len) elif key == Key.j: - operation(self.positions[0] + int(total), max_len) + operation(self._positions[0] + int(total), max_len) elif key in (Key.g, Key.G): operation(int(total) - 1, max_len) elif key in Key.digits(): @@ -295,4 +289,4 @@ def relative_to( def multiselect_all(self, max_len: int) -> None: """Set internal positions to entirity of list""" - self.positions = Positions(range(0, max_len)) + self._positions = Positions(range(0, max_len)) diff --git a/src/class_history.py b/src/class_history.py index 739fb5c..4b2db29 100644 --- a/src/class_history.py +++ b/src/class_history.py @@ -2,7 +2,20 @@ Helpers for storing and retrieving TodoList object records. """ -from src.class_todo import Todo, Todos, TodoList +from typing import NamedTuple + +from src.class_cursor import Cursor, Positions +from src.class_todo import Todo, Todos + + +class TodoList(NamedTuple): + """ + An object representing the todos + and a cursor (Positions) within the list + """ + + todos: Todos + cursor: Positions class _Restorable: @@ -12,9 +25,10 @@ class _Restorable: SEPARATOR = " |SEP|" - def __init__(self, todos: Todos, selected: int) -> None: + def __init__(self, todos: Todos, selected: Cursor) -> None: self.stored: str = self.SEPARATOR.join([repr(todo) for todo in todos]) - self.selected: int = selected + self.first: int = selected.get_first() + self.last: int = selected.get_last() def get(self) -> TodoList: """ @@ -23,11 +37,15 @@ def get(self) -> TodoList: Return the stored TodoList object. """ - stored = self.stored.split(self.SEPARATOR) - return TodoList(Todos([Todo(line) for line in stored]), self.selected) + return TodoList( + Todos([Todo(line) for line in self.stored.split(self.SEPARATOR)]), + Positions(range(self.first, self.last + 1)), + ) def __repr__(self) -> str: - return self.stored.replace(self.SEPARATOR, ", ") + f": {self.selected}" + return ( + self.stored.replace(self.SEPARATOR, ", ") + f": {self.first}..{self.last}" + ) class UndoRedo: @@ -36,39 +54,39 @@ class UndoRedo: """ def __init__(self) -> None: - self.history: list[_Restorable] = [] - self.index: int = -1 + self._history: list[_Restorable] = [] + self._index: int = -1 - def add(self, todos: Todos, selected: int) -> None: + def add(self, todos: Todos, selected: Cursor) -> None: """ Add a TodoList to the history. Backs up current state for potential retrieval later. """ - self.history.append(_Restorable(todos, selected)) - self.index = len(self.history) - 1 + self._history.append(_Restorable(todos, selected)) + self._index = len(self._history) - 1 def undo(self) -> TodoList: """ Return the previous TodoList state. """ - if self.index > 0: - self.index -= 1 - return self.history[self.index].get() + if self._index > 0: + self._index -= 1 + return self._history[self._index].get() def redo(self) -> TodoList: """ Return the next TodoList state, if it exists """ - if self.index < len(self.history) - 1: - self.index += 1 - return self.history[self.index].get() + if self._index < len(self._history) - 1: + self._index += 1 + return self._history[self._index].get() def __repr__(self) -> str: return ( "\n".join( - f"{'>' if i == self.index else ' '} {v}" - for i, v in enumerate(self.history) + f"{'>' if i == self._index else ' '} {v}" + for i, v in enumerate(self._history) ) - + f"\nlength: ({len(self.history)})\nindex: ({self.index})" + + f"\nlength: ({len(self._history)})\nindex: ({self._index})" ) diff --git a/src/class_todo.py b/src/class_todo.py index 17a56a5..ea96926 100644 --- a/src/class_todo.py +++ b/src/class_todo.py @@ -4,7 +4,7 @@ """ from enum import Enum -from typing import Iterable, NamedTuple +from typing import Iterable from src.get_args import CHECKBOX, INDENT from src.utils import Chunk, Color @@ -221,13 +221,3 @@ class Todos(list[Todo]): def __init__(self, iterable: Iterable[Todo]) -> None: super().__init__(iterable) - - -class TodoList(NamedTuple): - """ - An object representing the todos - and a cursor (int) within the list - """ - - todos: Todos - cursor: int diff --git a/todo.py b/todo.py index 7848725..8cbdf11 100755 --- a/todo.py +++ b/todo.py @@ -5,8 +5,8 @@ from sys import exit as sys_exit from typing import Callable, TypeAlias -from src.class_cursor import Cursor, Positions -from src.class_history import UndoRedo +from src.class_cursor import Cursor +from src.class_history import TodoList, UndoRedo from src.class_mode import SingleLineMode, SingleLineModeImpl from src.class_todo import BoxChar, FoldedState, Todo, Todos from src.clipboard import CLIPBOARD_EXISTS, copy_todo, paste_todo @@ -107,20 +107,20 @@ def move_todo(todos: Todos, selected: int, destination: int) -> Todos: return todos -def todo_up(todos: Todos, selected: Cursor) -> tuple[Todos, Positions]: +def todo_up(todos: Todos, selected: Cursor) -> TodoList: """Move the selected todo(s) up""" todos = move_todo(todos, selected.get_last(), selected.get_first() - 1) update_file(FILENAME, todos) selected.slide_up() - return todos, selected.get() + return TodoList(todos, selected.get()) -def todo_down(todos: Todos, selected: Cursor) -> tuple[Todos, Positions]: +def todo_down(todos: Todos, selected: Cursor) -> TodoList: """Move the selected todo(s) down""" todos = move_todo(todos, selected.get_first(), selected.get_last() + 1) update_file(FILENAME, todos) selected.slide_down(len(todos)) - return todos, selected.get() + return TodoList(todos, selected.get()) def new_todo_next( @@ -277,13 +277,13 @@ def _toggle_todo_note(todos: Todos, selected: Cursor) -> None: def _handle_undo(selected: Cursor, history: UndoRedo) -> Todos: - todos = selected.set_to_passthrough(history.undo()) + todos = selected.override_passthrough(*history.undo()) update_file(FILENAME, todos) return todos def _handle_redo(selected: Cursor, history: UndoRedo) -> Todos: - todos = selected.set_to_passthrough(history.redo()) + todos = selected.override_passthrough(*history.redo()) update_file(FILENAME, todos) return todos @@ -596,7 +596,7 @@ def main(stdscr: curses.window) -> int: Key.eight: (selected.relative_to, "stdscr, 8, len(todos), False"), Key.nine: (selected.relative_to, "stdscr, 9, len(todos), False"), } - history.add(todos, int(selected)) + history.add(todos, selected) _print_history(history) while True: @@ -642,7 +642,7 @@ def main(stdscr: curses.window) -> int: Key.ctrl_r, Key.u, ): # redo/undo - history.add(todos, int(selected)) + history.add(todos, selected) _print_history(history) edits -= 1