resizable, doesnt break when you provide too much text, implemented text wrapping

This commit is contained in:
Doman 2021-07-11 20:03:53 +02:00
parent 80d840a75a
commit 8099e86526
7 changed files with 184 additions and 123 deletions

View file

@ -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.

View file

@ -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:

View file

@ -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__("")

View file

@ -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)

View file

@ -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()

View file

@ -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():

View file

@ -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",