prepare for stats screen, initial drafts of the displayed component

This commit is contained in:
Doman 2022-06-02 00:54:58 +02:00
parent 8c00a8d8c9
commit bc40033c50
8 changed files with 117 additions and 65 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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