Skip to content

Commit 6d4a40f

Browse files
committed
chore: bump version to 1.3.5
- Fix LaneSettingsGroup layout initialization bug - Update defaults: fall_direction, enabled KeyViewer, lane keys - Increase settings window size to 400x670 - Add AGENTS.md development guidelines 💘 Generated with Crush Assisted-by: GLM-4.7 via Crush <crush@charm.land>
1 parent 7d80371 commit 6d4a40f

File tree

6 files changed

+228
-7
lines changed

6 files changed

+228
-7
lines changed

AGENTS.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# AGENTS.md
2+
3+
This file provides guidelines for agentic coding agents working in the RainingKeysPython repository.
4+
5+
## Build & Development Commands
6+
7+
### Running the Application
8+
```bash
9+
python main.py
10+
```
11+
12+
### Building Executable
13+
```bash
14+
python build.py
15+
```
16+
This creates both release and debug builds:
17+
- `RainingKeysPython.zip` - Release build (no console)
18+
- `RainingKeysPython-debug.zip` - Debug build (with console)
19+
20+
The build script automatically:
21+
1. Cleans previous build artifacts
22+
2. Creates release build with PyInstaller
23+
3. Creates debug build with console enabled
24+
4. Copies config.ini to dist directories
25+
5. Packages both builds into zip files
26+
27+
### Manual PyInstaller Build
28+
```bash
29+
pyinstaller RainingKeysPython.spec
30+
```
31+
32+
### Installing Dependencies
33+
```bash
34+
pip install -r requirements.txt
35+
```
36+
37+
## Project Overview
38+
39+
RainingKeysPython is a high-performance, external rhythm game input visualizer built with:
40+
- **Python 3.10+**
41+
- **PySide6 (Qt)** - GUI and rendering
42+
- **pynput** - Global keyboard monitoring
43+
- **pywin32** - Windows API for transparency/click-through
44+
45+
## Code Style Guidelines
46+
47+
### Imports
48+
- Import standard library modules first
49+
- Import third-party modules second
50+
- Import local modules last
51+
- Use relative imports within the core package (e.g., `from .configuration import AppConfig`)
52+
- Group imports by type with blank lines between sections
53+
54+
```python
55+
import time
56+
from collections import deque
57+
58+
from PySide6.QtWidgets import QWidget
59+
from pynput import keyboard
60+
61+
from .configuration import AppConfig
62+
```
63+
64+
### Type Hints
65+
- Use type hints for function signatures, especially for public methods
66+
- Use dataclasses for configuration objects
67+
- Type hint complex structures with `typing.Dict`, `typing.List`, etc.
68+
69+
```python
70+
from dataclasses import dataclass, field
71+
from typing import Dict, List
72+
73+
@dataclass
74+
class VisualSettings:
75+
scroll_speed: int = 800
76+
bar_color_str: str = "0,255,255,200"
77+
```
78+
79+
### Naming Conventions
80+
- **Classes**: PascalCase (e.g., `BarPool`, `RainingKeysOverlay`)
81+
- **Functions/Methods**: snake_case (e.g., `update_canvas`, `handle_input`)
82+
- **Constants**: UPPER_SNAKE_CASE (e.g., `MAX_BARS`, `FADE_START_Y`)
83+
- **Private methods**: Leading underscore (e.g., `_init_key_counts`)
84+
- **Instance variables**: snake_case (e.g., `active_bars`, `config`)
85+
86+
### Class Design
87+
- Use dataclasses for configuration objects
88+
- Use `__slots__` for performance-critical classes with many instances
89+
- Inherit from Qt classes (QWidget, QObject) appropriately
90+
- Use Qt's Signal/Slot pattern for inter-object communication
91+
92+
```python
93+
@dataclass
94+
class AppConfig:
95+
MAX_BARS: int = 300
96+
97+
class Bar:
98+
__slots__ = ['lane_index', 'press_time', 'release_time', 'active']
99+
```
100+
101+
### Error Handling
102+
- Use try/except with print statements for debugging (not logging module)
103+
- Provide fallback values for error conditions
104+
- Check for module availability with ImportError handling
105+
106+
```python
107+
try:
108+
import win32gui
109+
HAS_WIN32 = True
110+
except ImportError:
111+
HAS_WIN32 = False
112+
print("Warning: pywin32 not found.")
113+
```
114+
115+
### Qt/PySide6 Specifics
116+
- Initialize UI in `init_ui()` method
117+
- Use `super().__init__()` for all custom Qt widgets
118+
- Connect signals in constructor or init methods
119+
- Use `blockSignals(True/False)` when programmatically updating UI controls
120+
- Set window flags for overlay: `FramelessWindowHint | WindowStaysOnTopHint | Tool | WindowTransparentForInput`
121+
122+
### Performance Optimization
123+
- Use object pooling for frequently created/destroyed objects (see `BarPool` class)
124+
- Use `deque` for active/inactive object collections
125+
- Cache geometry calculations that don't change frequently
126+
- Use `time.perf_counter()` for high-precision timing
127+
128+
### Configuration Management
129+
- All config flows through `SettingsManager` and `AppConfig` dataclasses
130+
- Settings are persisted to `config.ini` using ConfigParser
131+
- Emit `settings_changed` Signal after any config modifications
132+
- Always save config after changes using `settings.save()`
133+
134+
### Thread Safety
135+
- Input monitoring runs in separate QThread (`InputMonitor`)
136+
- Use Qt Signals for thread communication (key_pressed, key_released)
137+
- Worker objects use `active_keys` set to filter autorepeat events
138+
139+
## Architecture Notes
140+
141+
### Core Components
142+
- `main.py` - Application entry point, initializes all components
143+
- `core/configuration.py` - Dataclasses for all config
144+
- `core/settings_manager.py` - Loads/saves config, emits change signals
145+
- `core/input_mon.py` - Global keyboard hook via pynput in QThread
146+
- `core/overlay.py` - Main rendering window, object pooling, paint logic
147+
- `core/gui.py` - Configuration UI window
148+
- `core/ui/components.py` - UI widget groups
149+
- `core/ui/theme.py` - Dark theme constants
150+
151+
### Key Patterns
152+
1. **Dependency Injection**: Pass `AppConfig` or `SettingsManager` to components
153+
2. **Signal-Slot**: Use Qt signals for loose coupling between components
154+
3. **Object Pooling**: Reuse Bar objects to avoid GC pressure
155+
4. **Configuration Separation**: Data holds values, SettingsManager handles I/O
156+
157+
## Testing
158+
159+
This project currently does not have formal tests configured. When adding tests:
160+
- Consider pytest as the test framework
161+
- Mock Qt components for unit tests
162+
- Test configuration persistence logic
163+
- Verify object pool behavior under load
164+
- Test input filtering (autorepeat prevention)
165+
166+
## File Structure
167+
168+
```
169+
core/
170+
├── __init__.py
171+
├── configuration.py # Dataclasses for all settings
172+
├── settings_manager.py # Config persistence, signal emission
173+
├── input_mon.py # Keyboard hook in QThread
174+
├── overlay.py # Main rendering window, paint logic
175+
├── gui.py # Settings UI window
176+
└── ui/
177+
├── __init__.py
178+
├── theme.py # Dark theme constants
179+
└── components.py # UI widget groups
180+
```
181+
182+
## Platform Notes
183+
184+
- **Windows Only**: Uses pywin32 for click-through transparency
185+
- Python 3.10+ required
186+
- Requires external overlay compatibility with game window

RELEASE_NOTES_1.3.5.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Release Notes - Version 1.3.5
2+
3+
## Bug Fixes
4+
- Fixed `NameError: name 'layout' is not defined` in LaneSettingsGroup
5+
- Moved `self.setLayout(layout)` from `update_from_config()` to `init_ui()` in core/ui/components.py:136
6+
7+
## UI Improvements
8+
- Increased settings window default size to 400×670 (was 340×500)
9+
10+
## Configuration Updates
11+
- Updated default configuration to match current config.ini values:
12+
- Added `fall_direction` setting with default value "up"
13+
- KeyViewer now enabled by default (was disabled)
14+
- Default lane keys set to d, f, j, k
15+
- SettingsManager now persists fall_direction setting
16+
- Debug mode disabled by default (production-ready)
17+
18+
## Technical Details
19+
- Added `__post_init__` method to AppConfig for default lane initialization
20+
- Improved settings persistence logic in SettingsManager
21+
22+
## Files Changed
23+
- core/configuration.py - Updated defaults and added __post_init__
24+
- core/settings_manager.py - Added fall_direction load/save support
25+
- core/gui.py - Increased window size
26+
- core/ui/components.py - Fixed layout initialization bug
27+
28+
## Build Artifacts
29+
- RainingKeysPython.zip - Release build (no console)
30+
- RainingKeysPython-debug.zip - Debug build (with console)

core/configuration.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class VisualSettings:
99
bar_width: int = 70
1010
bar_height: int = 20
1111
bar_color_str: str = "0,255,255,200"
12+
fall_direction: str = "up"
1213

1314
@property
1415
def bar_color(self) -> QColor:
@@ -25,7 +26,7 @@ class PositionSettings:
2526

2627
@dataclass
2728
class KeyViewerSettings:
28-
enabled: bool = False
29+
enabled: bool = True
2930
layout: str = "horizontal"
3031
panel_position: str = "below"
3132
panel_offset_x: int = 0
@@ -47,9 +48,11 @@ class AppConfig:
4748
FADE_START_Y: int = 800
4849
FADE_RANGE: int = 200
4950
DEBUG_MODE: bool = False
50-
VERSION: str = "1.3.0"
51+
VERSION: str = "1.3.5"
5152

52-
# Derived properties could go here, but kept simple for now
53+
def __post_init__(self):
54+
if not self.lane_map:
55+
self.lane_map = {'d': 0, 'f': 1, 'j': 2, 'k': 3}
5356

5457
def set_lane_keys(self, keys: List[str]):
5558
new_map = {}

core/gui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def __init__(self, settings_manager: SettingsManager):
1313
self.settings = settings_manager
1414
self.config = self.settings.app_config
1515
self.setWindowTitle(f"RainingKeys Config v{self.config.VERSION}")
16-
self.resize(340, 500)
16+
self.resize(400, 670)
1717

1818
# Apply Theme
1919
self.setStyleSheet(DARK_THEME)

core/settings_manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def load(self):
2222
if self.config_parser.has_section('Visual'):
2323
self.app_config.visual.scroll_speed = self.config_parser.getint('Visual', 'scroll_speed', fallback=800)
2424
self.app_config.visual.bar_color_str = self.config_parser.get('Visual', 'bar_color', fallback="0,255,255,200")
25+
self.app_config.visual.fall_direction = self.config_parser.get('Visual', 'fall_direction', fallback="up")
2526

2627
# Position
2728
if self.config_parser.has_section('Position'):
@@ -31,7 +32,7 @@ def load(self):
3132
# KeyViewer
3233
if self.config_parser.has_section('keyviewer'):
3334
kv = self.app_config.key_viewer
34-
kv.enabled = self.config_parser.getboolean('keyviewer', 'enabled', fallback=False)
35+
kv.enabled = self.config_parser.getboolean('keyviewer', 'enabled', fallback=True)
3536
kv.layout = self.config_parser.get('keyviewer', 'layout', fallback="horizontal")
3637
kv.panel_position = self.config_parser.get('keyviewer', 'panel_position', fallback="below")
3738
kv.panel_offset_x = self.config_parser.getint('keyviewer', 'panel_offset_x', fallback=0)
@@ -56,6 +57,7 @@ def save(self):
5657
if not self.config_parser.has_section('Visual'): self.config_parser.add_section('Visual')
5758
self.config_parser.set('Visual', 'scroll_speed', str(self.app_config.visual.scroll_speed))
5859
self.config_parser.set('Visual', 'bar_color', self.app_config.visual.bar_color_str)
60+
self.config_parser.set('Visual', 'fall_direction', self.app_config.visual.fall_direction)
5961

6062
# Position
6163
if not self.config_parser.has_section('Position'): self.config_parser.add_section('Position')

core/ui/components.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ def init_ui(self):
133133
self.lbl_instruction.setStyleSheet("color: gray; font-size: 10px;")
134134
layout.addWidget(self.lbl_instruction)
135135

136+
self.setLayout(layout)
137+
136138
def update_from_config(self):
137139
self.update_status("Current Keys: " + str(len(self.config.lane_map)))
138140

139-
self.setLayout(layout)
140-
141141
def toggle_recording(self):
142142
self.is_recording = not self.is_recording
143143
if self.is_recording:

0 commit comments

Comments
 (0)