From 8099e86526958ace2afe9589a9d42be936d67ec8 Mon Sep 17 00:00:00 2001 From: Doman Date: Sun, 11 Jul 2021 20:03:53 +0200 Subject: [PATCH] resizable, doesnt break when you provide too much text, implemented text wrapping --- README.md | 8 +- fasttyper/__main__.py | 25 +++--- fasttyper/buffer.py | 75 ++++------------ fasttyper/components.py | 191 +++++++++++++++++++++++++++++++--------- fasttyper/interface.py | 2 - fasttyper/state.py | 4 +- setup.py | 2 +- 7 files changed, 184 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 22df528..ef4a305 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ _Fasttyper_ is ran as an python module, so to execute it simply type: `python3 -m fasttyper` -from cloned github repository, if you didn't [install](#installation) package from TestPyPi. Without any argument program waits for user to enter text manually and then signal the end of it with keyboard interrupt (CTRL+C). _Fasttyper_ can open text files, which path should be provided as first and only argument to the module execution, for example: +from cloned github repository, if you didn't [install](#installation) package from TestPyPi. + +_Fasttyper_ can open text files, which path should be provided as first and only argument to the module execution, for example: `python3 -m fasttyper example_text.txt` @@ -33,6 +35,4 @@ You can use another similar projects set of words as well, for example to create `curl -s https://raw.githubusercontent.com/Miodec/monkeytype/master/static/languages/english.json | python3 -c "import sys, json; print('\n'.join(json.load(sys.stdin)['words']))" | shuf -n20 | awk 1 ORS=' ' | python3 -m fasttyper` -# Known issues - -Too large text brakes the app. +To exit program simply complete test or press CTRL+C. diff --git a/fasttyper/__main__.py b/fasttyper/__main__.py index 6f27c31..81ecc8d 100644 --- a/fasttyper/__main__.py +++ b/fasttyper/__main__.py @@ -1,6 +1,12 @@ from .application import Application from .interface import Interface -from .components import UserInput, CursorComponent, ReferenceText, StatsComponent +from .components import ( + UserInput, + CursorComponent, + ReferenceText, + StatsComponent, + TextBox, +) from .listener import Listener from .buffer import UserBuffer, Buffer from .config import Config @@ -18,21 +24,20 @@ def initialize(configmap, rbuffer): reference_buffer = Buffer(rbuffer) user_buffer = UserBuffer() - cursor_component = CursorComponent() - user_input = UserInput(cursor_component) - reference_text = ReferenceText() - stats_component = StatsComponent() + cursor_component = CursorComponent(config) + text_box = TextBox(config, cursor_component) + user_input = UserInput(config, text_box) + reference_text = ReferenceText(config, text_box) + stats_component = StatsComponent(config) listener = Listener() application = Application(listener, user_buffer, reference_buffer, config) interface = Interface( application, - [user_input, reference_text, stats_component, cursor_component], + [user_input, reference_text, text_box, stats_component, cursor_component], ) wrapper(interface) - user_buffer.close() - reference_buffer.close() application.summarize() @@ -56,14 +61,14 @@ def main(): if is_tty: with open(os.path.expanduser(args.file)) as f: - rbuffer = io.StringIO(f.read()) + rbuffer = f.read() else: input_lines = sys.stdin.readlines() with open("/dev/tty") as f: os.dup2(f.fileno(), 0) - rbuffer = io.StringIO("".join(input_lines)) + rbuffer = "".join(input_lines) try: with open(os.path.expanduser(args.config)) as f: diff --git a/fasttyper/buffer.py b/fasttyper/buffer.py index eb731cf..9818366 100644 --- a/fasttyper/buffer.py +++ b/fasttyper/buffer.py @@ -1,46 +1,27 @@ -import io from .listener import Action class Buffer: def __init__(self, buffer): - self.buffer = buffer - self._strip_end() + self.buffer = " ".join(buffer.split()) def _write(self, data): - self.buffer.write(data) + self.buffer += data def _del_char(self): - pos = self.buffer.tell() - - if pos > 0: - self.buffer.seek(pos - 1) - self.buffer.truncate() - return True + self.buffer = self.buffer[:-1] def _last_char(self): - pos = self.buffer.tell() - - if pos > 0: - self.buffer.seek(pos - 1) - return self.buffer.read(1) + if len(self.buffer) > 0: + return self.buffer[-1] def _del_word(self): - found_word = False + words = self.buffer.split() - while True: - last_char = self._last_char() - - if last_char is None: - break - - if last_char in (" ", "\n") and found_word: - break - - if last_char.isalnum() and not found_word: - found_word = True - - self._del_char() + if len(words) > 1: + self.buffer = " ".join(words[:-1]) + " " + else: + self.buffer = "" def handle_action(self, action, char): if action == Action.add_char: @@ -55,44 +36,18 @@ class Buffer: self._del_word() def get_matrix(self, position=0): - self.buffer.seek(position) - return self.buffer.read() + return self.buffer[position:] def get_position(self): - return self.buffer.tell() + return len(self.buffer) def get_lenght(self): - position = self.get_position() - self.buffer.read() - lenght = self.get_position() - self.buffer.seek(position) - return lenght + return len(self.buffer) def read(self, position): - _position = self.get_position() - self.buffer.seek(position) - char = self._last_char() - self.buffer.seek(_position) - return char - - def close(self): - self.buffer.close() - - def _strip_end(self): - self.buffer.read() - while True: - last_char = self._last_char() - - if last_char is None: - break - - if last_char.isalnum(): - break - - self._del_char() - self.buffer.seek(0) + return self.buffer[position] class UserBuffer(Buffer): def __init__(self): - super().__init__(io.StringIO()) + super().__init__("") diff --git a/fasttyper/components.py b/fasttyper/components.py index 2e3699e..eb56110 100644 --- a/fasttyper/components.py +++ b/fasttyper/components.py @@ -2,19 +2,12 @@ import curses class Base: - def __init__(self): - self.rows = None - self.cols = None - - def init(self, screen, application): - self.rows, self.cols = screen.getmaxyx() - def paint(self, screen, application): pass class CursorComponent(Base): - def __init__(self): + def __init__(self, config): super().__init__() self.cursor_position = None @@ -25,57 +18,167 @@ class CursorComponent(Base): screen.move(*self.cursor_position) -class UserInput(Base): - def __init__(self, cursor_component): +class TextComponent(Base): + def paint_text(self, screen, text, color): + screen.addstr(text, curses.color_pair(color)) + + +class StatsComponent(TextComponent): + def __init__(self, config): super().__init__() - self.cursor_component = cursor_component - self.valid_color = None - self.invalid_color = None + self.color = config.get("stats_color") + self.template = config.get("stats_template") def init(self, screen, application): super().init(screen, application) - self.valid_color = application.config.get("user_input_valid_color") - self.invalid_color = application.config.get("user_input_invalid_color") + + def paint(self, screen, application): + usedy, _ = screen.getyx() + maxy, _ = screen.getmaxyx() + + text = self.template.format(stats=application.stats) + + texty = len(text.splitlines()) + + if texty + usedy + 1 < maxy: + self.paint_text(screen, text, self.color) + + +class TextBox(TextComponent): + """ + Wraps lines of text elements writing to it nicely + """ + + class Element: + def __init__(self, text, color, triggers_cursor=False): + self.text = text + self.color = color + self.triggers_cursor = triggers_cursor + self.next = None + + def __init__(self, config, cursor_component): + super().__init__() + self.cursor_component = cursor_component + + self.elements = [] + + self.maxy, self.maxx = None, None + self.usedy, self.usedx = None, None + + def clear(self): + self.elements = [] + + self.maxy, self.maxx = None, None + self.usedy, self.usedx = None, None + + def add_element(self, text, color, triggers_cursor=False): + element = self.Element(text, color, triggers_cursor) + + if self.elements: + self.elements[-1].next = element + + self.elements.append(element) + + def find_first_word(self, element): + if element is None: + return "" + + if len(element.text) == 0: + return self.find_first_word(element.next) + + if not element.text[0].isalnum(): + return "" + + return element.text.split()[0] + " " + + @property + def max_line_x(self): + return self.maxx - 1 + + def prepare_text_element(self, element): + lines = [] + line = "" + word = "" + next_word = self.find_first_word(element.next) + + for char in element.text + next_word: + if char.isalnum(): + word += char + elif char == "\n" or len(word) + len(line) + self.usedx >= self.max_line_x: + lines.append(line) + word += " " + line = "" + line += word + word = "" + self.usedx = 0 + if self.usedy >= self.maxy: + break + else: + word += char + line += word + word = "" + + if len(word) > 0: + if len(word) + len(line) + self.usedx >= self.max_line_x: + lines.append(line) + line = word + else: + line += word + + if len(line) > 0: + lines.append(line) + + if len(lines) == 0 or (len(lines) == 1 and lines[0] == next_word): + return "" + + if next_word: + last_line = lines[-1] + if len(last_line) != 0: + lines[-1] = lines[-1][: -len(next_word)] + + lines = lines[: self.maxy - self.usedy] + return "\n".join(lines) + + def pain_element(self, screen, element): + self.usedy, self.usedx = screen.getyx() + self.maxy, self.maxx = screen.getmaxyx() + text = self.prepare_text_element(element) + self.paint_text(screen, text, element.color) + + if element.triggers_cursor: + self.cursor_component.update(screen) + + def paint(self, screen, application): + + for element in self.elements: + self.pain_element(screen, element) + + self.clear() + + +class UserInput(Base): + def __init__(self, config, text_box): + super().__init__() + self.text_box = text_box + self.valid_color = config.get("user_input_valid_color") + self.invalid_color = config.get("user_input_invalid_color") def paint(self, screen, application): valid_text, invalid_text = application.get_user_text() invalid_text = invalid_text.replace(" ", "_") - invalid_text = invalid_text.replace("\n", "\\n") - screen.addstr(valid_text, curses.color_pair(self.valid_color)) - screen.addstr(invalid_text, curses.color_pair(self.invalid_color)) - self.cursor_component.update(screen) + self.text_box.add_element(valid_text, self.valid_color) + self.text_box.add_element(invalid_text, self.invalid_color, True) class ReferenceText(Base): - def __init__(self): + def __init__(self, config, text_box): super().__init__() - self.color = None - - def init(self, screen, application): - super().init(screen, application) - self.color = application.config.get("reference_text_color") + self.text_box = text_box + self.color = config.get("reference_text_color") def paint(self, screen, application): valid_user_text_position = application.valid_user_text_position() reference_text = application.get_reference_text(valid_user_text_position) - reference_text = reference_text.replace("\n", "\\n\n") - screen.addstr(reference_text, curses.color_pair(self.color)) - - -class StatsComponent(Base): - def __init__(self): - super().__init__() - self.color = None - self.template = None - - def init(self, screen, application): - super().init(screen, application) - self.color = application.config.get("stats_color") - self.template = application.config.get("stats_template") - - def paint(self, screen, application): - screen.addstr( - self.template.format(stats=application.stats), curses.color_pair(self.color) - ) + self.text_box.add_element(reference_text, self.color) diff --git a/fasttyper/interface.py b/fasttyper/interface.py index 37f6c47..5889452 100644 --- a/fasttyper/interface.py +++ b/fasttyper/interface.py @@ -16,8 +16,6 @@ class Interface: def init(self, screen): self.init_colors() - for component in self.components: - component.init(screen, self.application) def update(self, screen): screen.clear() diff --git a/fasttyper/state.py b/fasttyper/state.py index bca3b33..7015658 100644 --- a/fasttyper/state.py +++ b/fasttyper/state.py @@ -19,12 +19,12 @@ class StateMachine: user_position = self.application.user_buffer.get_position() if self.state == State.invalid: if action in (Action.del_char, Action.del_word): - if user_position == self.mistake_position: + if user_position <= self.mistake_position: self.mistake_position = None self.state = State.valid if self.state == State.valid: if action in (Action.add_char, Action.add_space, Action.add_newline): - if char != self.application.reference_buffer.read(user_position): + if char != self.application.reference_buffer.read(user_position - 1): self.mistake_position = user_position - 1 self.state = State.invalid elif user_position == self.application.reference_buffer.get_lenght(): diff --git a/setup.py b/setup.py index a713c48..4752068 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh: setup( name="fasttyper-pkg-ickyicky", - version="0.0.1", + version="0.0.2", author="Piotr Domanski", author_email="pi.domanski@gmail.com", description="Minimalistic typing exercise",