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.
@@ -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,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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,3 @@
1
+ from .tui import main
2
+
3
+ raise SystemExit(main())
@@ -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()
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,2 @@
1
+ [console_scripts]
2
+ vibetype = vibetype.tui:main
@@ -0,0 +1 @@
1
+ textual>=8.2.7
@@ -0,0 +1 @@
1
+ vibetype