From bc40033c506ce9584a7e293098e5c3598ddaf837 Mon Sep 17 00:00:00 2001 From: Doman Date: Thu, 2 Jun 2022 00:54:58 +0200 Subject: [PATCH] prepare for stats screen, initial drafts of the displayed component --- fasttyper/__main__.py | 10 ++++-- fasttyper/application.py | 15 ++++----- fasttyper/cli.py | 20 ++++++++++-- fasttyper/components.py | 70 ++++++++++++++++++++++++++-------------- fasttyper/config.py | 18 ++--------- fasttyper/interface.py | 13 +++++++- fasttyper/runner.py | 12 +++++-- fasttyper/stats.py | 24 +++++++++----- 8 files changed, 117 insertions(+), 65 deletions(-) diff --git a/fasttyper/__main__.py b/fasttyper/__main__.py index 0c9e21e..54fcf71 100644 --- a/fasttyper/__main__.py +++ b/fasttyper/__main__.py @@ -1,4 +1,4 @@ -from .cli import initialize, get_parser +from .cli import initialize, get_parser, RuntimeConfig import sys import os @@ -23,7 +23,13 @@ def main(): rbuffer = "".join(input_lines) - initialize(args.config, rbuffer, args.unclutter_backspace, args.no_cursor) + initialize( + args.config, + rbuffer, + args.unclutter_backspace, + args.no_cursor, + RuntimeConfig(mode=RuntimeConfig.TEXT), + ) if __name__ == "__main__": diff --git a/fasttyper/application.py b/fasttyper/application.py index 298c1d7..b3db2f9 100644 --- a/fasttyper/application.py +++ b/fasttyper/application.py @@ -22,22 +22,19 @@ class Application: def action(self, screen): try: action, key = self.listener.listen(screen) - self.buffer.handle_action(action, key) + if self.running(): + self.buffer.handle_action(action, key) except StoppingSignal as e: if e.silent: self.silent_exit = True - self.buffer.stats.signal_stop() + if self.running(): + self.buffer.stats.signal_stop() def summarize(self): if self.finished: - self.buffer.stats.summarize(self.config.get("summary_template")) self.buffer.stats.export_to_datafile(self.config.get("summary_datafile")) - try: - c = readchar.readchar() - if ord(c) == 3: - raise KeyboardInterrupt - except KeyboardInterrupt: - sys.exit(1) + return True + return False def exit(self): if not self.finished and not self.silent_exit: diff --git a/fasttyper/cli.py b/fasttyper/cli.py index 6b19644..c2c8958 100644 --- a/fasttyper/cli.py +++ b/fasttyper/cli.py @@ -2,6 +2,7 @@ from .application import Application from .interface import Interface from .components import ( TextBox, + StatsBox, ) from .listener import Listener from .buffer import Buffer @@ -13,7 +14,17 @@ import argparse import json -def initialize(config_path, rbuffer, backspace_debug, no_cursor): +class RuntimeConfig: + WORDS = "words" + TEXT = "text" + + def __init__(self, words=None, language=None, mode=None): + self.words = words + self.language = language + self.mode = mode + + +def initialize(config_path, rbuffer, backspace_debug, no_cursor, runtime_config): try: with open(os.path.expanduser(config_path)) as f: configmap = json.load(f) @@ -23,7 +34,8 @@ def initialize(config_path, rbuffer, backspace_debug, no_cursor): config = Config(configmap) text_box = TextBox(config) - stats = Stats() + stats_box = StatsBox(config) + stats = Stats(runtime_config) buffer = Buffer(rbuffer, text_box, stats) @@ -35,11 +47,13 @@ def initialize(config_path, rbuffer, backspace_debug, no_cursor): [ text_box, ], + [ + stats_box, + ], no_cursor, ) wrapper(interface) - application.summarize() application.exit() diff --git a/fasttyper/components.py b/fasttyper/components.py index 5deca76..65c27eb 100644 --- a/fasttyper/components.py +++ b/fasttyper/components.py @@ -68,6 +68,7 @@ class BorderedBox(WindowComponent): self.pos_x = config.get("left_margin_percentage") / 100 self.height = config.get("lines_on_screen") self.width = None + self.application = None def paint_text(self, row, col, text, color): super().paint_text(row + 1, col + 1, text, color) @@ -76,6 +77,7 @@ class BorderedBox(WindowComponent): super().move(x + 1, y + 1) def init(self, screen, application): + self.application = application self.width = int(self.maxx * (1 - 2 * self.pos_x)) self.update_size( self.height + 2, @@ -89,7 +91,30 @@ class BorderedBox(WindowComponent): screen.refresh() -class BufferDependentComponent(BorderedBox): +class BorderWithImprintedStats(BorderedBox): + """ + Imprints stats on one of borders + """ + + def __init__(self, config): + super().__init__(config) + + self.stats_template = config.get("stats_template").replace("\n", " ") + self.stats_color = config.get("stats_color") + self.stats_row = -1 if config.get("stats_position") == "top" else self.height + + def paint_stats(self): + text = self.stats_template.format(stats=self.application.buffer.stats) + if len(text) < self.width - 2: + self.paint_text( + self.stats_row, + 2, + text + " " * (self.width - 2 - len(text)), + self.stats_color, + ) + + +class BufferDependentComponent(BorderWithImprintedStats): """ Adds content source from buffer """ @@ -276,30 +301,7 @@ class BufferDependentComponent(BorderedBox): self.update_cursor() -class BorderWithImprintedStats(BufferDependentComponent): - """ - Imprints stats on one of borders - """ - - def __init__(self, config): - super().__init__(config) - - self.stats_template = config.get("stats_template").replace("\n", " ") - self.stats_color = config.get("stats_color") - self.stats_row = -1 if config.get("stats_position") == "top" else self.height - - def paint_stats(self): - text = self.stats_template.format(stats=self.buffer.stats) - if len(text) < self.width - 2: - self.paint_text( - self.stats_row, - 2, - text + " " * (self.width - 2 - len(text)), - self.stats_color, - ) - - -class TextBox(BorderWithImprintedStats): +class TextBox(BufferDependentComponent): """ Calls all inherited paint functions and inits windows """ @@ -322,3 +324,21 @@ class TextBox(BorderWithImprintedStats): self.move(self.cursor_x, self.cursor_y) self.refresh() + + +class StatsBox(BorderWithImprintedStats): + """ + Displays stats + """ + + def __init__(self, config): + super().__init__(config) + self.wpm_color = config.get("wpm_grapth_color") + self.raw_wpm_color = config.get("raw_wpm_grapth_color") + self.errors = config.get("errors_graph_color") + + def paint(self, screen, application): + self.maxy, self.maxx = screen.getmaxyx() + self.init(screen, application) + self.paint_stats() + self.refresh() diff --git a/fasttyper/config.py b/fasttyper/config.py index 4beda27..4cb445d 100644 --- a/fasttyper/config.py +++ b/fasttyper/config.py @@ -6,25 +6,13 @@ class Config: "stats_template": "wpm: {stats.wpm:0.2f}, time: {stats.total_seconds:0.2f}s", "stats_color": 5, "stats_position": "top", - "summary_template": ( - "WPM: {stats.wpm:0.2f}\n" - "CPM: {stats.cpm:0.2f}\n" - "RAW WPM: {stats.raw_wpm:0.2f}\n" - "RAW CPM: {stats.raw_cpm:0.2f}\n" - "total seconds: {stats.total_seconds:0.2f}\n" - "total minutes: {stats.total_minutes:0.2f}\n" - "correct words: {stats.correct_words}\n" - "correct chars: {stats.correct_chars}\n" - "incorrect words: {stats.incorrect_words}\n" - "incorrect chars: {stats.incorrect_chars}\n" - "total words: {stats.total_words}\n" - "total chars: {stats.total_chars}\n" - "accuracy: {stats.accuracy:0.2f}%" - ), "summary_datafile": "~/.cache/fasttyper/datafile.csv", "top_margin_percentage": 40, "left_margin_percentage": 35, "lines_on_screen": 3, + "wpm_graph_color": 3, + "raw_wpm_graph_color": 9, + "errors_graph_color": 2, } def __init__(self, configmap): diff --git a/fasttyper/interface.py b/fasttyper/interface.py index 18c0b76..a6c2d79 100644 --- a/fasttyper/interface.py +++ b/fasttyper/interface.py @@ -2,9 +2,10 @@ import curses class Interface: - def __init__(self, application, components, no_cursor=False): + def __init__(self, application, components, end_components, no_cursor=False): self.application = application self.components = components + self.end_components = end_components self.no_cursor = no_cursor self.colors = True @@ -29,6 +30,10 @@ class Interface: for component in self.components: component.paint(screen, self.application) + def draw_end(self, screen): + for component in self.end_components: + component.paint(screen, self.application) + def __call__(self, screen): """ Main running loop @@ -42,3 +47,9 @@ class Interface: while self.application.running(): self.update(screen) self.application.action(screen) + + if self.application.summarize(): + screen.clear() + curses.curs_set(0) + self.draw_end(screen) + self.application.action(screen) diff --git a/fasttyper/runner.py b/fasttyper/runner.py index eb89030..6a270e1 100644 --- a/fasttyper/runner.py +++ b/fasttyper/runner.py @@ -1,4 +1,4 @@ -from .cli import initialize, get_parser +from .cli import initialize, get_parser, RuntimeConfig from random import choice import os import requests @@ -48,7 +48,15 @@ def runner(): while True: rbuffer = " ".join([choice(words) for _ in range(args.amount)]) - initialize(args.config, rbuffer, args.unclutter_backspace, args.no_cursor) + initialize( + args.config, + rbuffer, + args.unclutter_backspace, + args.no_cursor, + RuntimeConfig( + mode=RuntimeConfig.WORDS, words=args.amount, language=args.language + ), + ) if __name__ == "__main__": diff --git a/fasttyper/stats.py b/fasttyper/stats.py index 853a676..81b42a9 100644 --- a/fasttyper/stats.py +++ b/fasttyper/stats.py @@ -6,7 +6,9 @@ import pathlib class Stats: - def __init__(self): + def __init__(self, runtime_config): + self.runtime_config = runtime_config + self.correct_chars = 0 self.incorrect_chars = 0 @@ -16,6 +18,8 @@ class Stats: self.buffer = None self.finished = False + self.snaps = [] + def set_buffer(self, buffer): self.buffer = buffer @@ -29,14 +33,20 @@ class Stats: def signal_valid(self): self.signal_running() self.correct_chars += 1 + self.take_snap() def signal_invalid(self): self.signal_running() self.incorrect_chars += 1 + self.take_snap() def signal_stop(self, finished=False): self.stop_dtime = datetime.now() self.finished = finished + self.take_snap() + + def take_snap(self): + self.snaps.append(self.produce_record()) @property def correct_words(self): @@ -86,13 +96,10 @@ class Stats: return self.correct_chars / self.total_chars * 100 return 100 - def summarize(self, template): - print(template.format(stats=self)) - def produce_record(self): return { - "start_dtime": self.start_dtime.isoformat(), - "stop_dtime": self.stop_dtime.isoformat(), + "start_dtime": self.start_dtime.isoformat() if self.start_dtime else None, + "stop_dtime": self.stop_dtime.isoformat() if self.stop_dtime else None, "total_seconds": self.total_seconds, "total_minutes": self.total_minutes, "total_chars": self.total_chars, @@ -106,6 +113,9 @@ class Stats: "raw_wpm": self.raw_wpm, "raw_cpm": self.raw_cpm, "accuracy": self.accuracy, + "mode": self.runtime_config.mode, + "language": self.runtime_config.language, + "words": self.runtime_config.words, } def export_to_datafile(self, datafile): @@ -129,5 +139,3 @@ class Stats: with open(datafile, "a") as f: writter = csv.DictWriter(f, fieldnames=record.keys()) writter.writerow(record) - - print(f"\nwrote stats to {datafile}")