vibetype 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vibetype-0.1.0/PKG-INFO +90 -0
- vibetype-0.1.0/README.md +82 -0
- vibetype-0.1.0/pyproject.toml +29 -0
- vibetype-0.1.0/setup.cfg +4 -0
- vibetype-0.1.0/vibetype/__init__.py +0 -0
- vibetype-0.1.0/vibetype/__main__.py +3 -0
- vibetype-0.1.0/vibetype/base.py +19 -0
- vibetype-0.1.0/vibetype/data/script.py +96 -0
- vibetype-0.1.0/vibetype/data/vibetype_data.db +0 -0
- vibetype-0.1.0/vibetype/screens/__init__.py +0 -0
- vibetype-0.1.0/vibetype/screens/about.py +15 -0
- vibetype-0.1.0/vibetype/screens/menu.py +53 -0
- vibetype-0.1.0/vibetype/screens/mode.py +49 -0
- vibetype-0.1.0/vibetype/screens/results.py +38 -0
- vibetype-0.1.0/vibetype/screens/show_stat.py +137 -0
- vibetype-0.1.0/vibetype/screens/type_space.py +199 -0
- vibetype-0.1.0/vibetype/tui.py +32 -0
- vibetype-0.1.0/vibetype/typelearn.tcss +70 -0
- vibetype-0.1.0/vibetype.egg-info/PKG-INFO +90 -0
- vibetype-0.1.0/vibetype.egg-info/SOURCES.txt +22 -0
- vibetype-0.1.0/vibetype.egg-info/dependency_links.txt +1 -0
- vibetype-0.1.0/vibetype.egg-info/entry_points.txt +2 -0
- vibetype-0.1.0/vibetype.egg-info/requires.txt +1 -0
- vibetype-0.1.0/vibetype.egg-info/top_level.txt +1 -0
vibetype-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vibetype
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: textual>=8.2.7
|
|
8
|
+
|
|
9
|
+
# VibeType
|
|
10
|
+
|
|
11
|
+
An offline, keyboard-first typing application built with Python and Textual.
|
|
12
|
+
|
|
13
|
+
VibeType is designed as a distraction-free space to type for fun, improve speed, and optionally learn through themed sentence packs.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
### Typing Modes
|
|
18
|
+
|
|
19
|
+
- Random word practice
|
|
20
|
+
- Themed sentence packs
|
|
21
|
+
- DSA
|
|
22
|
+
- Pokémon
|
|
23
|
+
- Anime Quotes
|
|
24
|
+
|
|
25
|
+
### Statistics
|
|
26
|
+
|
|
27
|
+
- Persistent local statistics
|
|
28
|
+
- Category-wise filtering
|
|
29
|
+
- Stores WPM, Raw WPM and Accuracy
|
|
30
|
+
- Average and maximum values
|
|
31
|
+
- All-time
|
|
32
|
+
- Latest 10 tests
|
|
33
|
+
|
|
34
|
+
### Philosophy
|
|
35
|
+
|
|
36
|
+
- Offline first
|
|
37
|
+
- Keyboard-first interface
|
|
38
|
+
- No accounts
|
|
39
|
+
- No telemetry
|
|
40
|
+
|
|
41
|
+
## Tech Stack
|
|
42
|
+
|
|
43
|
+
- Python
|
|
44
|
+
- Textual
|
|
45
|
+
- Rich
|
|
46
|
+
- SQLite
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
Clone the repository:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/BartikaKumar/vibetype.git
|
|
54
|
+
cd vibetype
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Using uv
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv sync
|
|
61
|
+
uv run vibetype
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Using pip
|
|
65
|
+
|
|
66
|
+
(venv optional but recommended)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python3 -m venv .venv
|
|
70
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Install and run:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install .
|
|
77
|
+
vibetype
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Alternatively, without installing the package:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install -r requirements.txt
|
|
84
|
+
python3 -m vibetype
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Future Ideas
|
|
88
|
+
|
|
89
|
+
- Custom sentence packs
|
|
90
|
+
- Import/export custom csvs of sentence packs
|
vibetype-0.1.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# VibeType
|
|
2
|
+
|
|
3
|
+
An offline, keyboard-first typing application built with Python and Textual.
|
|
4
|
+
|
|
5
|
+
VibeType is designed as a distraction-free space to type for fun, improve speed, and optionally learn through themed sentence packs.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
### Typing Modes
|
|
10
|
+
|
|
11
|
+
- Random word practice
|
|
12
|
+
- Themed sentence packs
|
|
13
|
+
- DSA
|
|
14
|
+
- Pokémon
|
|
15
|
+
- Anime Quotes
|
|
16
|
+
|
|
17
|
+
### Statistics
|
|
18
|
+
|
|
19
|
+
- Persistent local statistics
|
|
20
|
+
- Category-wise filtering
|
|
21
|
+
- Stores WPM, Raw WPM and Accuracy
|
|
22
|
+
- Average and maximum values
|
|
23
|
+
- All-time
|
|
24
|
+
- Latest 10 tests
|
|
25
|
+
|
|
26
|
+
### Philosophy
|
|
27
|
+
|
|
28
|
+
- Offline first
|
|
29
|
+
- Keyboard-first interface
|
|
30
|
+
- No accounts
|
|
31
|
+
- No telemetry
|
|
32
|
+
|
|
33
|
+
## Tech Stack
|
|
34
|
+
|
|
35
|
+
- Python
|
|
36
|
+
- Textual
|
|
37
|
+
- Rich
|
|
38
|
+
- SQLite
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Clone the repository:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/BartikaKumar/vibetype.git
|
|
46
|
+
cd vibetype
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Using uv
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv sync
|
|
53
|
+
uv run vibetype
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Using pip
|
|
57
|
+
|
|
58
|
+
(venv optional but recommended)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python3 -m venv .venv
|
|
62
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Install and run:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install .
|
|
69
|
+
vibetype
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Alternatively, without installing the package:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -r requirements.txt
|
|
76
|
+
python3 -m vibetype
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Future Ideas
|
|
80
|
+
|
|
81
|
+
- Custom sentence packs
|
|
82
|
+
- Import/export custom csvs of sentence packs
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vibetype"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Add your description here"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"textual>=8.2.7",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
vibetype="vibetype.tui:main"
|
|
17
|
+
|
|
18
|
+
[tool.uv]
|
|
19
|
+
package=true
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where=["."]
|
|
23
|
+
include=["vibetype*"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
vibetype=[
|
|
27
|
+
"*.tcss",
|
|
28
|
+
"data/*.db",
|
|
29
|
+
]
|
vibetype-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from textual.screen import Screen
|
|
2
|
+
from textual.widgets import Footer, Static
|
|
3
|
+
|
|
4
|
+
class BaseScreen(Screen):
|
|
5
|
+
|
|
6
|
+
def action_close_screen(self):
|
|
7
|
+
self.app.pop_screen()
|
|
8
|
+
|
|
9
|
+
BINDINGS=[
|
|
10
|
+
('escape','close_screen','Return to Menu'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
def compose(self):
|
|
14
|
+
yield Static("VibeType",classes='header')
|
|
15
|
+
yield from self.compose_body()
|
|
16
|
+
yield Footer()
|
|
17
|
+
|
|
18
|
+
def compose_body(self):
|
|
19
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import sqlite3
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
|
|
6
|
+
conn_data=sqlite3.connect('vibetype_data.db')
|
|
7
|
+
|
|
8
|
+
cursor=conn_data.cursor()
|
|
9
|
+
|
|
10
|
+
with open("random.csv", newline="", encoding="utf-8") as f:
|
|
11
|
+
reader = csv.DictReader(f)
|
|
12
|
+
|
|
13
|
+
rows = [
|
|
14
|
+
(r["word"],)
|
|
15
|
+
for r in reader
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
cursor.execute("""
|
|
19
|
+
CREATE TABLE IF NOT EXISTS random(
|
|
20
|
+
id INTEGER PRIMARY KEY,
|
|
21
|
+
word TEXT
|
|
22
|
+
)
|
|
23
|
+
""")
|
|
24
|
+
|
|
25
|
+
cursor.executemany("""
|
|
26
|
+
INSERT INTO random(word)
|
|
27
|
+
VALUES (?)
|
|
28
|
+
""", rows)
|
|
29
|
+
|
|
30
|
+
with open("anime.csv", newline="", encoding="utf-8") as f:
|
|
31
|
+
reader1 = csv.DictReader(f)
|
|
32
|
+
|
|
33
|
+
rows1 = [
|
|
34
|
+
(r["sentence"],r["details"])
|
|
35
|
+
for r in reader1
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
with open("dsa.csv", newline="", encoding="utf-8") as f:
|
|
39
|
+
reader2 = csv.DictReader(f)
|
|
40
|
+
|
|
41
|
+
rows2 = [
|
|
42
|
+
(r["sentence"],"")
|
|
43
|
+
for r in reader2
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
with open("pokemon.csv", newline="", encoding="utf-8") as f:
|
|
47
|
+
reader3 = csv.DictReader(f)
|
|
48
|
+
|
|
49
|
+
rows3 = [
|
|
50
|
+
(r["sentence"],"")
|
|
51
|
+
for r in reader3
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
cursor.execute("""
|
|
55
|
+
CREATE TABLE IF NOT EXISTS anime(
|
|
56
|
+
id INTEGER PRIMARY KEY,
|
|
57
|
+
sentence TEXT,
|
|
58
|
+
details TEXT
|
|
59
|
+
)
|
|
60
|
+
""")
|
|
61
|
+
|
|
62
|
+
cursor.execute("""
|
|
63
|
+
CREATE TABLE IF NOT EXISTS dsa(
|
|
64
|
+
id INTEGER PRIMARY KEY,
|
|
65
|
+
sentence TEXT,
|
|
66
|
+
details TEXT
|
|
67
|
+
)
|
|
68
|
+
""")
|
|
69
|
+
|
|
70
|
+
cursor.execute("""
|
|
71
|
+
CREATE TABLE IF NOT EXISTS pokemon(
|
|
72
|
+
id INTEGER PRIMARY KEY,
|
|
73
|
+
sentence TEXT,
|
|
74
|
+
details TEXT
|
|
75
|
+
)
|
|
76
|
+
""")
|
|
77
|
+
|
|
78
|
+
cursor.executemany("""
|
|
79
|
+
INSERT INTO dsa(sentence,details)
|
|
80
|
+
VALUES (?,?)
|
|
81
|
+
""", rows2)
|
|
82
|
+
|
|
83
|
+
cursor.executemany("""
|
|
84
|
+
INSERT INTO pokemon(sentence,details)
|
|
85
|
+
VALUES (?,?)
|
|
86
|
+
""", rows3)
|
|
87
|
+
|
|
88
|
+
cursor.executemany("""
|
|
89
|
+
INSERT INTO anime(sentence,details)
|
|
90
|
+
VALUES (?,?)
|
|
91
|
+
""", rows1)
|
|
92
|
+
|
|
93
|
+
conn_data.commit()
|
|
94
|
+
|
|
95
|
+
if __name__=="__main__":
|
|
96
|
+
main()
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from vibetype.base import BaseScreen
|
|
2
|
+
|
|
3
|
+
from textual.containers import VerticalScroll
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
|
|
6
|
+
class ShowAbout(BaseScreen):
|
|
7
|
+
|
|
8
|
+
def compose_body(self):
|
|
9
|
+
with VerticalScroll(classes='cont about'):
|
|
10
|
+
yield Static(
|
|
11
|
+
"Welcome to VibeType!\n\n"
|
|
12
|
+
"Here you can type away with themed sentence packs, or just random words that make no sense. This is an offline, keyboard-first space that's entirely yours.\n\n"
|
|
13
|
+
"P.S. For learning-based themes, typing is not enough! Read and sit with the sentence and its related fact before moving on to the next one :)",
|
|
14
|
+
classes='welcome'
|
|
15
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from vibetype.base import BaseScreen
|
|
2
|
+
|
|
3
|
+
from textual.containers import Vertical, CenterMiddle
|
|
4
|
+
from textual.widgets import OptionList
|
|
5
|
+
from textual.widgets.option_list import Option
|
|
6
|
+
|
|
7
|
+
from .about import ShowAbout
|
|
8
|
+
from .mode import ChooseMode
|
|
9
|
+
from .show_stat import ShowStat
|
|
10
|
+
|
|
11
|
+
class MenuScreen(BaseScreen):
|
|
12
|
+
|
|
13
|
+
def action_show_about(self):
|
|
14
|
+
self.app.push_screen(ShowAbout())
|
|
15
|
+
|
|
16
|
+
def action_choose_mode(self):
|
|
17
|
+
self.app.push_screen(ChooseMode())
|
|
18
|
+
|
|
19
|
+
def action_show_stat(self):
|
|
20
|
+
self.app.push_screen(ShowStat())
|
|
21
|
+
|
|
22
|
+
def action_close_app(self):
|
|
23
|
+
self.app.conn_data.close()
|
|
24
|
+
self.app.conn_stats.close()
|
|
25
|
+
self.app.exit()
|
|
26
|
+
|
|
27
|
+
BINDINGS=[
|
|
28
|
+
('escape','close_app','Exit'),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def on_option_list_option_selected(self,event: OptionList.OptionSelected):
|
|
32
|
+
option_id=event.option.id
|
|
33
|
+
if option_id=='exit_app':
|
|
34
|
+
self.action_close_app()
|
|
35
|
+
elif option_id=='show_about':
|
|
36
|
+
self.action_show_about()
|
|
37
|
+
elif option_id=='show_stat':
|
|
38
|
+
self.action_show_stat()
|
|
39
|
+
elif option_id=='start_type':
|
|
40
|
+
self.action_choose_mode()
|
|
41
|
+
|
|
42
|
+
def compose_body(self):
|
|
43
|
+
with CenterMiddle():
|
|
44
|
+
with Vertical(classes='cont'):
|
|
45
|
+
yield OptionList(
|
|
46
|
+
Option('About',id='show_about'),
|
|
47
|
+
None,
|
|
48
|
+
Option('Start',id='start_type'),
|
|
49
|
+
None,
|
|
50
|
+
Option('Statistics',id='show_stat'),
|
|
51
|
+
None,
|
|
52
|
+
Option('Exit',id='exit_app'),
|
|
53
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from vibetype.base import BaseScreen
|
|
2
|
+
|
|
3
|
+
from textual.containers import Vertical, CenterMiddle
|
|
4
|
+
from textual.widgets import OptionList
|
|
5
|
+
from textual.widgets.option_list import Option
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from .type_space import StartType
|
|
9
|
+
|
|
10
|
+
class ChooseMode(BaseScreen):
|
|
11
|
+
|
|
12
|
+
def opentype(self,mode='random'):
|
|
13
|
+
self.app.switch_screen(StartType(mode))
|
|
14
|
+
|
|
15
|
+
def on_option_list_option_selected(self,event: OptionList.OptionSelected):
|
|
16
|
+
list_id=event.option_list.id
|
|
17
|
+
option_id=event.option.id
|
|
18
|
+
|
|
19
|
+
if list_id=='main_modes':
|
|
20
|
+
if option_id=='mode_random':
|
|
21
|
+
self.opentype('random')
|
|
22
|
+
elif option_id=='mode_themed':
|
|
23
|
+
event.option_list.add_class('hidden')
|
|
24
|
+
self.query_one('#theme_modes').remove_class('hidden')
|
|
25
|
+
|
|
26
|
+
elif list_id=='theme_modes':
|
|
27
|
+
if option_id=='mode_dsa':
|
|
28
|
+
self.opentype('dsa')
|
|
29
|
+
elif option_id=='mode_pokemon':
|
|
30
|
+
self.opentype('pokemon')
|
|
31
|
+
elif option_id=='mode_anime':
|
|
32
|
+
self.opentype('anime')
|
|
33
|
+
|
|
34
|
+
def compose_body(self):
|
|
35
|
+
with CenterMiddle():
|
|
36
|
+
with Vertical(classes='cont'):
|
|
37
|
+
yield OptionList(
|
|
38
|
+
Option('Random Words',id='mode_random'),
|
|
39
|
+
None,
|
|
40
|
+
Option('Themed Sentence Packs',id='mode_themed'),
|
|
41
|
+
id='main_modes')
|
|
42
|
+
|
|
43
|
+
yield OptionList(
|
|
44
|
+
Option('DSA',id='mode_dsa'),
|
|
45
|
+
None,
|
|
46
|
+
Option('Pokemon',id='mode_pokemon'),
|
|
47
|
+
None,
|
|
48
|
+
Option('Anime',id='mode_anime'),
|
|
49
|
+
id='theme_modes',classes='hidden')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from vibetype.base import BaseScreen
|
|
2
|
+
|
|
3
|
+
from textual.containers import VerticalScroll, CenterMiddle
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
from .type_space import StartType
|
|
6
|
+
|
|
7
|
+
class ResultScreen(BaseScreen):
|
|
8
|
+
|
|
9
|
+
def action_close_screen(self):
|
|
10
|
+
self.app.sesh_max=None
|
|
11
|
+
self.app.sesh_min=None
|
|
12
|
+
super().action_close_screen()
|
|
13
|
+
|
|
14
|
+
def action_next(self):
|
|
15
|
+
from .type_space import StartType # lazy import
|
|
16
|
+
self.app.switch_screen(StartType(self.mode))
|
|
17
|
+
|
|
18
|
+
BINDINGS=[
|
|
19
|
+
('enter','next','Next Sentence'),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def __init__(self,mode,sentence='',details='',*,raw_wpm,wpm,accuracy):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.mode=mode
|
|
25
|
+
|
|
26
|
+
self.sentence=sentence
|
|
27
|
+
self.details=details
|
|
28
|
+
self.raw_wpm=raw_wpm
|
|
29
|
+
self.wpm=wpm
|
|
30
|
+
self.accuracy=accuracy
|
|
31
|
+
|
|
32
|
+
def compose_body(self):
|
|
33
|
+
with CenterMiddle():
|
|
34
|
+
with VerticalScroll(classes='cont results'):
|
|
35
|
+
yield Static(self.sentence)
|
|
36
|
+
if(self.details):
|
|
37
|
+
yield Static(self.details)
|
|
38
|
+
yield Static(f'WPM: {self.wpm}\nRaw WPM: {self.raw_wpm}\nAccuracy: {self.accuracy}%')
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from vibetype.base import BaseScreen
|
|
2
|
+
|
|
3
|
+
from textual.containers import VerticalScroll, Vertical, CenterMiddle, HorizontalScroll
|
|
4
|
+
from textual.widgets import Static, Select
|
|
5
|
+
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
class ShowStat(BaseScreen):
|
|
9
|
+
|
|
10
|
+
def action_focus_filter(self):
|
|
11
|
+
select=self.query_one(".dropdown")
|
|
12
|
+
#select.focus()
|
|
13
|
+
select.action_show_overlay()
|
|
14
|
+
|
|
15
|
+
BINDINGS=[
|
|
16
|
+
("tab","focus_filter","Open Stats Filter")
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def load_stats(self,mode=None):
|
|
20
|
+
cursor=self.app.conn_stats.cursor()
|
|
21
|
+
cursor.execute("""
|
|
22
|
+
CREATE TABLE IF NOT EXISTS stats (
|
|
23
|
+
id INTEGER PRIMARY KEY,
|
|
24
|
+
mode TEXT NOT NULL,
|
|
25
|
+
wpm REAL NOT NULL,
|
|
26
|
+
raw_wpm REAL NOT NULL,
|
|
27
|
+
accuracy REAL NOT NULL,
|
|
28
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
29
|
+
)
|
|
30
|
+
""")
|
|
31
|
+
self.app.conn_stats.commit()
|
|
32
|
+
if mode is None or mode=="all":
|
|
33
|
+
all_stats=cursor.execute("""
|
|
34
|
+
SELECT
|
|
35
|
+
ROUND(AVG(wpm),2),
|
|
36
|
+
MAX(wpm),
|
|
37
|
+
ROUND(AVG(raw_wpm),2),
|
|
38
|
+
MAX(raw_wpm),
|
|
39
|
+
ROUND(AVG(accuracy),2),
|
|
40
|
+
MAX(accuracy),
|
|
41
|
+
COUNT(*)
|
|
42
|
+
FROM stats;
|
|
43
|
+
""").fetchone()
|
|
44
|
+
latest_stats=cursor.execute("""
|
|
45
|
+
SELECT
|
|
46
|
+
ROUND(AVG(wpm),2),
|
|
47
|
+
MAX(wpm),
|
|
48
|
+
ROUND(AVG(raw_wpm),2),
|
|
49
|
+
MAX(raw_wpm),
|
|
50
|
+
ROUND(AVG(accuracy),2),
|
|
51
|
+
MAX(accuracy),
|
|
52
|
+
COUNT(*)
|
|
53
|
+
FROM(
|
|
54
|
+
SELECT *
|
|
55
|
+
FROM stats
|
|
56
|
+
ORDER BY created_at DESC
|
|
57
|
+
LIMIT 10
|
|
58
|
+
);
|
|
59
|
+
""").fetchone()
|
|
60
|
+
else:
|
|
61
|
+
all_stats=cursor.execute("""
|
|
62
|
+
SELECT
|
|
63
|
+
ROUND(AVG(wpm),2),
|
|
64
|
+
MAX(wpm),
|
|
65
|
+
ROUND(AVG(raw_wpm),2),
|
|
66
|
+
MAX(raw_wpm),
|
|
67
|
+
ROUND(AVG(accuracy),2),
|
|
68
|
+
MAX(accuracy),
|
|
69
|
+
COUNT(*)
|
|
70
|
+
FROM stats
|
|
71
|
+
WHERE mode=?;
|
|
72
|
+
""",(mode,)).fetchone()
|
|
73
|
+
latest_stats=cursor.execute("""
|
|
74
|
+
SELECT
|
|
75
|
+
ROUND(AVG(wpm),2),
|
|
76
|
+
MAX(wpm),
|
|
77
|
+
ROUND(AVG(raw_wpm),2),
|
|
78
|
+
MAX(raw_wpm),
|
|
79
|
+
ROUND(AVG(accuracy),2),
|
|
80
|
+
MAX(accuracy),
|
|
81
|
+
COUNT(*)
|
|
82
|
+
FROM(
|
|
83
|
+
SELECT *
|
|
84
|
+
FROM stats
|
|
85
|
+
WHERE mode=?
|
|
86
|
+
ORDER BY created_at DESC
|
|
87
|
+
LIMIT 10
|
|
88
|
+
);
|
|
89
|
+
""",(mode,)).fetchone()
|
|
90
|
+
|
|
91
|
+
self.count.update(f"Tests: {all_stats[6]}")
|
|
92
|
+
|
|
93
|
+
all_table = Table(title="All-time")
|
|
94
|
+
all_table.add_column("Metric", justify="left")
|
|
95
|
+
all_table.add_column("Avg", justify="right")
|
|
96
|
+
all_table.add_column("Max", justify="right")
|
|
97
|
+
|
|
98
|
+
l_table = Table(title="Latest 10")
|
|
99
|
+
l_table.add_column("Metric", justify="left")
|
|
100
|
+
l_table.add_column("Avg", justify="right")
|
|
101
|
+
l_table.add_column("Max", justify="right")
|
|
102
|
+
|
|
103
|
+
all_table.add_row("WPM",str(all_stats[0]),str(all_stats[1]))
|
|
104
|
+
all_table.add_row("Raw WPM",str(all_stats[2]),str(all_stats[3]))
|
|
105
|
+
all_table.add_row("Accuracy",str(all_stats[4]),str(all_stats[5]))
|
|
106
|
+
|
|
107
|
+
l_table.add_row("WPM",str(latest_stats[0]),str(latest_stats[1]))
|
|
108
|
+
l_table.add_row("Raw WPM",str(latest_stats[2]),str(latest_stats[3]))
|
|
109
|
+
l_table.add_row("Accuracy",str(latest_stats[4]),str(latest_stats[5]))
|
|
110
|
+
|
|
111
|
+
self.all_time.update(all_table)
|
|
112
|
+
self.latest.update(l_table)
|
|
113
|
+
|
|
114
|
+
def on_mount(self) -> None:
|
|
115
|
+
self.count=self.query_one("#count")
|
|
116
|
+
self.all_time=self.query_one("#all_time")
|
|
117
|
+
self.latest=self.query_one("#latest")
|
|
118
|
+
self.load_stats()
|
|
119
|
+
|
|
120
|
+
def on_select_changed(self,event:Select.Changed):
|
|
121
|
+
self.load_stats(event.value)
|
|
122
|
+
|
|
123
|
+
def compose_body(self):
|
|
124
|
+
with CenterMiddle():
|
|
125
|
+
with VerticalScroll(classes='stats cont'):
|
|
126
|
+
with Vertical():
|
|
127
|
+
yield Select([
|
|
128
|
+
("All modes","all"),
|
|
129
|
+
("Random","random"),
|
|
130
|
+
("DSA","dsa"),
|
|
131
|
+
("Pokemon","pokemon"),
|
|
132
|
+
("Anime","anime")
|
|
133
|
+
],value="all",allow_blank=False,classes="dropdown",compact=True)
|
|
134
|
+
yield Static("",id="count")
|
|
135
|
+
with HorizontalScroll(classes="horizontal"):
|
|
136
|
+
yield Static("",id="all_time")
|
|
137
|
+
yield Static("",id="latest")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from vibetype.base import BaseScreen
|
|
2
|
+
|
|
3
|
+
from textual.containers import Vertical, CenterMiddle
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
class StartType(BaseScreen):
|
|
11
|
+
|
|
12
|
+
def action_close_screen(self):
|
|
13
|
+
self.app.sesh_max=None
|
|
14
|
+
self.app.sesh_min=None
|
|
15
|
+
super().action_close_screen()
|
|
16
|
+
|
|
17
|
+
def load_screen(self,wpm=None,raw_wpm=None,accuracy=None):
|
|
18
|
+
self.curr=''
|
|
19
|
+
self.word=Text()
|
|
20
|
+
|
|
21
|
+
cursor=self.app.conn_data.cursor()
|
|
22
|
+
if self.app.sesh_min is None:
|
|
23
|
+
self.app.sesh_min,self.app.sesh_max= cursor.execute(f"SELECT MIN(id), MAX(id) FROM {self.mode}").fetchone()
|
|
24
|
+
|
|
25
|
+
if self.mode=='random':
|
|
26
|
+
ids=random.sample(range(self.app.sesh_min,self.app.sesh_max+1),self.app.random_words)
|
|
27
|
+
placeholders=",".join("?"*len(ids))
|
|
28
|
+
rows=cursor.execute(f"SELECT word FROM {self.mode} WHERE id IN ({placeholders})",ids).fetchall()
|
|
29
|
+
self.sentence=" ".join(row[0] for row in rows)
|
|
30
|
+
self.details=""
|
|
31
|
+
|
|
32
|
+
else:
|
|
33
|
+
id=random.randint(self.app.sesh_min,self.app.sesh_max)
|
|
34
|
+
row=cursor.execute(f"SELECT * FROM {self.mode} WHERE id={id}").fetchone()
|
|
35
|
+
self.sentence=row[1]
|
|
36
|
+
self.details=row[2]
|
|
37
|
+
|
|
38
|
+
self.target=self.sentence.split(' ')
|
|
39
|
+
self.typed=[None]*len(self.target)
|
|
40
|
+
self.typed_str=['']*len(self.target)
|
|
41
|
+
|
|
42
|
+
self.at_word=0
|
|
43
|
+
|
|
44
|
+
self.correct=0
|
|
45
|
+
self.incorrect=0
|
|
46
|
+
|
|
47
|
+
self.start_time=None
|
|
48
|
+
|
|
49
|
+
if wpm is not None:
|
|
50
|
+
self.query_one(".header").update(f"VibeType | WPM: {wpm} | Raw WPM: {raw_wpm} | Accuracy: {accuracy}")
|
|
51
|
+
|
|
52
|
+
self.target_widget.update(Text(self.sentence+' ',style=str(self.app.theme_variables['primary-muted'])))
|
|
53
|
+
|
|
54
|
+
def save_stats(self,wpm,raw_wpm,accuracy):
|
|
55
|
+
cursor=self.app.conn_stats.cursor()
|
|
56
|
+
cursor.execute("""
|
|
57
|
+
CREATE TABLE IF NOT EXISTS stats (
|
|
58
|
+
id INTEGER PRIMARY KEY,
|
|
59
|
+
mode TEXT NOT NULL,
|
|
60
|
+
wpm REAL NOT NULL,
|
|
61
|
+
raw_wpm REAL NOT NULL,
|
|
62
|
+
accuracy REAL NOT NULL,
|
|
63
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
64
|
+
)
|
|
65
|
+
""")
|
|
66
|
+
cursor.execute("""
|
|
67
|
+
INSERT INTO stats(mode,wpm,raw_wpm,accuracy)
|
|
68
|
+
VALUES(?,?,?,?)
|
|
69
|
+
""",(self.mode,wpm,raw_wpm,accuracy))
|
|
70
|
+
self.app.conn_stats.commit()
|
|
71
|
+
|
|
72
|
+
def __init__(self, mode='random'): # runs before compose
|
|
73
|
+
super().__init__()
|
|
74
|
+
self.mode=mode
|
|
75
|
+
|
|
76
|
+
def on_mount(self): # runs after compose
|
|
77
|
+
self.target_widget=self.query_one("#target")
|
|
78
|
+
self.load_screen()
|
|
79
|
+
|
|
80
|
+
def render_text(self):
|
|
81
|
+
|
|
82
|
+
full=Text()
|
|
83
|
+
to_match=self.target[self.at_word]
|
|
84
|
+
|
|
85
|
+
pword=0
|
|
86
|
+
|
|
87
|
+
while(pword<len(self.target)):
|
|
88
|
+
|
|
89
|
+
if pword<self.at_word:
|
|
90
|
+
full.append_text(self.typed[pword])
|
|
91
|
+
|
|
92
|
+
elif pword==self.at_word:
|
|
93
|
+
|
|
94
|
+
p=0
|
|
95
|
+
self.word=Text()
|
|
96
|
+
|
|
97
|
+
while(p<len(self.curr)):
|
|
98
|
+
if(p<len(to_match)):
|
|
99
|
+
if(self.curr[p]!=self.target[self.at_word][p]):
|
|
100
|
+
self.word.append(self.curr[p],style=str(self.app.theme_variables['error']))
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
self.word.append(self.curr[p],style=str(self.app.theme_variables['primary']))
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
self.word.append(self.curr[p],style=str(self.app.theme_variables['error-muted']))
|
|
107
|
+
|
|
108
|
+
p+=1
|
|
109
|
+
|
|
110
|
+
while(p<len(to_match)):
|
|
111
|
+
self.word.append(to_match[p],style=str(self.app.theme_variables['primary-muted']))
|
|
112
|
+
p+=1
|
|
113
|
+
|
|
114
|
+
full.append_text(self.word)
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
full.append(self.target[pword],style=str(self.app.theme_variables['primary-muted']))
|
|
118
|
+
|
|
119
|
+
full.append(' ')
|
|
120
|
+
pword+=1
|
|
121
|
+
|
|
122
|
+
self.target_widget.update(full)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def on_key(self,event):
|
|
127
|
+
|
|
128
|
+
modified=False
|
|
129
|
+
|
|
130
|
+
if event.key=='backspace':
|
|
131
|
+
if len(self.curr)>0:
|
|
132
|
+
self.curr = self.curr[:-1]
|
|
133
|
+
modified=True
|
|
134
|
+
|
|
135
|
+
elif len(self.curr)==0 and self.at_word>0:
|
|
136
|
+
|
|
137
|
+
self.at_word-=1
|
|
138
|
+
|
|
139
|
+
self.word=self.typed[self.at_word].copy()
|
|
140
|
+
self.curr=self.typed_str[self.at_word]
|
|
141
|
+
|
|
142
|
+
modified=True
|
|
143
|
+
|
|
144
|
+
elif event.key=='space':
|
|
145
|
+
if self.curr and self.at_word<len(self.target)-1:
|
|
146
|
+
|
|
147
|
+
if len(self.curr)>=len(self.target[self.at_word]):
|
|
148
|
+
self.correct+=1
|
|
149
|
+
else:
|
|
150
|
+
self.incorrect+=1
|
|
151
|
+
|
|
152
|
+
self.typed[self.at_word]=self.word.copy()
|
|
153
|
+
self.typed_str[self.at_word]=self.curr
|
|
154
|
+
|
|
155
|
+
self.curr=''
|
|
156
|
+
|
|
157
|
+
self.word=Text()
|
|
158
|
+
self.at_word+=1
|
|
159
|
+
|
|
160
|
+
modified=True
|
|
161
|
+
|
|
162
|
+
elif event.character and event.character.isprintable():
|
|
163
|
+
self.curr=self.curr+event.character
|
|
164
|
+
|
|
165
|
+
if len(self.curr)<=len(self.target[self.at_word]) and event.character==self.target[self.at_word][len(self.curr)-1]:
|
|
166
|
+
self.correct+=1
|
|
167
|
+
else:
|
|
168
|
+
self.incorrect+=1
|
|
169
|
+
|
|
170
|
+
modified=True
|
|
171
|
+
|
|
172
|
+
self.render_text()
|
|
173
|
+
|
|
174
|
+
if modified and self.start_time is None:
|
|
175
|
+
self.start_time=time.perf_counter()
|
|
176
|
+
|
|
177
|
+
if self.at_word==len(self.target)-1 and self.curr==self.target[-1]:
|
|
178
|
+
|
|
179
|
+
duration=time.perf_counter()-self.start_time
|
|
180
|
+
|
|
181
|
+
raw_wpm=round((self.correct+self.incorrect)*12/duration,2)
|
|
182
|
+
wpm=round(self.correct*12/duration,2)
|
|
183
|
+
accuracy=round(self.correct*100/(self.correct+self.incorrect),2)
|
|
184
|
+
|
|
185
|
+
self.typed[self.at_word] = self.word.copy()
|
|
186
|
+
self.typed_str[self.at_word]=self.curr
|
|
187
|
+
|
|
188
|
+
self.save_stats(wpm,raw_wpm,accuracy)
|
|
189
|
+
|
|
190
|
+
if(self.mode=="random"):
|
|
191
|
+
self.load_screen(wpm,raw_wpm,accuracy)
|
|
192
|
+
else:
|
|
193
|
+
from .results import ResultScreen # lazy import to prevent circular import issue
|
|
194
|
+
self.app.switch_screen(ResultScreen(self.mode,self.sentence,self.details,raw_wpm=raw_wpm,wpm=wpm,accuracy=accuracy))
|
|
195
|
+
|
|
196
|
+
def compose_body(self):
|
|
197
|
+
with CenterMiddle():
|
|
198
|
+
with Vertical(classes='cont'):
|
|
199
|
+
yield Static('',id='target')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from textual.app import App
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sqlite3
|
|
5
|
+
|
|
6
|
+
from .screens.menu import MenuScreen
|
|
7
|
+
|
|
8
|
+
class VibeType(App):
|
|
9
|
+
ENABLE_COMMAND_PALETTE=False
|
|
10
|
+
TITLE = "VibeType"
|
|
11
|
+
|
|
12
|
+
def on_mount(self) -> None:
|
|
13
|
+
self.theme = "catppuccin-mocha"
|
|
14
|
+
|
|
15
|
+
self.conn_data=sqlite3.connect(Path(__file__).parent / 'data' / 'vibetype_data.db')
|
|
16
|
+
self.conn_stats=sqlite3.connect(Path(__file__).parent / 'data' / 'vibetype_stats.db')
|
|
17
|
+
|
|
18
|
+
self.sesh_min=None
|
|
19
|
+
self.sesh_max=None
|
|
20
|
+
|
|
21
|
+
self.random_words=10
|
|
22
|
+
|
|
23
|
+
self.push_screen(MenuScreen())
|
|
24
|
+
|
|
25
|
+
CSS_PATH='typelearn.tcss'
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
app = VibeType()
|
|
29
|
+
app.run()
|
|
30
|
+
|
|
31
|
+
if __name__=="__main__":
|
|
32
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
.cont{
|
|
2
|
+
width:100%;
|
|
3
|
+
margin:1;
|
|
4
|
+
align:center middle;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.about{
|
|
8
|
+
height:auto;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.header{
|
|
12
|
+
dock: top;
|
|
13
|
+
height: auto;
|
|
14
|
+
content-align: center middle;
|
|
15
|
+
background: $footer-background;
|
|
16
|
+
color:$footer-foreground;
|
|
17
|
+
text-align:center;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
OptionList{
|
|
21
|
+
height:auto;
|
|
22
|
+
width:auto;
|
|
23
|
+
align:center middle;
|
|
24
|
+
background:transparent;
|
|
25
|
+
border:none;
|
|
26
|
+
text-align:center;
|
|
27
|
+
text-style:none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
OptionList .option-list--option-highlighted{
|
|
31
|
+
text-style:bold;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.hidden{
|
|
35
|
+
display:None;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#target{
|
|
39
|
+
text-align:center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.results Static{
|
|
43
|
+
text-align:center;
|
|
44
|
+
margin:1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.stats Vertical{
|
|
48
|
+
align:center middle;
|
|
49
|
+
height:auto;
|
|
50
|
+
margin-bottom:1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.stats Static{
|
|
54
|
+
text-align:center;
|
|
55
|
+
margin:1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.horizontal Static{
|
|
59
|
+
margin-left:1;
|
|
60
|
+
margin-right:1;
|
|
61
|
+
width:auto;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.horizontal{
|
|
65
|
+
align:center middle;
|
|
66
|
+
height:auto;
|
|
67
|
+
}
|
|
68
|
+
.dropdown{
|
|
69
|
+
width:20;
|
|
70
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vibetype
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: textual>=8.2.7
|
|
8
|
+
|
|
9
|
+
# VibeType
|
|
10
|
+
|
|
11
|
+
An offline, keyboard-first typing application built with Python and Textual.
|
|
12
|
+
|
|
13
|
+
VibeType is designed as a distraction-free space to type for fun, improve speed, and optionally learn through themed sentence packs.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
### Typing Modes
|
|
18
|
+
|
|
19
|
+
- Random word practice
|
|
20
|
+
- Themed sentence packs
|
|
21
|
+
- DSA
|
|
22
|
+
- Pokémon
|
|
23
|
+
- Anime Quotes
|
|
24
|
+
|
|
25
|
+
### Statistics
|
|
26
|
+
|
|
27
|
+
- Persistent local statistics
|
|
28
|
+
- Category-wise filtering
|
|
29
|
+
- Stores WPM, Raw WPM and Accuracy
|
|
30
|
+
- Average and maximum values
|
|
31
|
+
- All-time
|
|
32
|
+
- Latest 10 tests
|
|
33
|
+
|
|
34
|
+
### Philosophy
|
|
35
|
+
|
|
36
|
+
- Offline first
|
|
37
|
+
- Keyboard-first interface
|
|
38
|
+
- No accounts
|
|
39
|
+
- No telemetry
|
|
40
|
+
|
|
41
|
+
## Tech Stack
|
|
42
|
+
|
|
43
|
+
- Python
|
|
44
|
+
- Textual
|
|
45
|
+
- Rich
|
|
46
|
+
- SQLite
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
Clone the repository:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/BartikaKumar/vibetype.git
|
|
54
|
+
cd vibetype
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Using uv
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv sync
|
|
61
|
+
uv run vibetype
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Using pip
|
|
65
|
+
|
|
66
|
+
(venv optional but recommended)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python3 -m venv .venv
|
|
70
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Install and run:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install .
|
|
77
|
+
vibetype
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Alternatively, without installing the package:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install -r requirements.txt
|
|
84
|
+
python3 -m vibetype
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Future Ideas
|
|
88
|
+
|
|
89
|
+
- Custom sentence packs
|
|
90
|
+
- Import/export custom csvs of sentence packs
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
vibetype/__init__.py
|
|
4
|
+
vibetype/__main__.py
|
|
5
|
+
vibetype/base.py
|
|
6
|
+
vibetype/tui.py
|
|
7
|
+
vibetype/typelearn.tcss
|
|
8
|
+
vibetype.egg-info/PKG-INFO
|
|
9
|
+
vibetype.egg-info/SOURCES.txt
|
|
10
|
+
vibetype.egg-info/dependency_links.txt
|
|
11
|
+
vibetype.egg-info/entry_points.txt
|
|
12
|
+
vibetype.egg-info/requires.txt
|
|
13
|
+
vibetype.egg-info/top_level.txt
|
|
14
|
+
vibetype/data/script.py
|
|
15
|
+
vibetype/data/vibetype_data.db
|
|
16
|
+
vibetype/screens/__init__.py
|
|
17
|
+
vibetype/screens/about.py
|
|
18
|
+
vibetype/screens/menu.py
|
|
19
|
+
vibetype/screens/mode.py
|
|
20
|
+
vibetype/screens/results.py
|
|
21
|
+
vibetype/screens/show_stat.py
|
|
22
|
+
vibetype/screens/type_space.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
textual>=8.2.7
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vibetype
|