resizable, doesnt break when you provide too much text, implemented text wrapping
This commit is contained in:
parent
80d840a75a
commit
8099e86526
7 changed files with 184 additions and 123 deletions
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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__("")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():
|
||||
|
|
2
setup.py
2
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",
|
||||
|
|
Loading…
Reference in a new issue