11from dataclasses import dataclass , field
22from typing import Dict , List , Optional
33from PySide6 .QtGui import QColor
4+ from PySide6 .QtWidgets import QApplication
45from .logging_config import get_logger
56
67logger = get_logger (__name__ )
78
9+
10+ @dataclass
11+ class DisplayConstants :
12+ """Display and layout constants for the overlay."""
13+ LANE_START_X : int = 50
14+ EXTRA_PADDING : int = 50
15+ KEYVIEWER_OFFSET_Y_TOP : int = 50
16+ KEYVIEWER_OFFSET_Y_BOTTOM : int = 50
17+ FALLBACK_SCREEN_HEIGHT : int = 1080
18+ FADE_START_Y : int = 800
19+ FADE_RANGE : int = 200
20+
821@dataclass
922class VisualSettings :
23+ """Visual settings for bar appearance and behavior."""
1024 scroll_speed : int = 800
1125 lane_width : int = 70
1226 bar_width : int = 70
1327 bar_height : int = 20
1428 bar_color_str : str = "0,255,255,200"
1529 fall_direction : str = "up"
1630
17- # Display constants
18- LANE_START_X : int = 50
19- EXTRA_PADDING : int = 50
20- KEYVIEWER_OFFSET_Y_TOP : int = 50
21- KEYVIEWER_OFFSET_Y_BOTTOM : int = 50
22- FALLBACK_SCREEN_HEIGHT : int = 1080
23-
2431 @property
2532 def bar_color (self ) -> QColor :
26- """Parse and validate color string, returning QColor."""
33+ """Parse and validate color string, returning QColor.
34+
35+ Returns:
36+ QColor object parsed from bar_color_str.
37+
38+ Raises:
39+ ValueError: If color string format is invalid.
40+ """
2741 try :
2842 parts = self .bar_color_str .split (',' )
2943 if len (parts ) != 4 :
@@ -36,38 +50,62 @@ def bar_color(self) -> QColor:
3650
3751 return QColor (r , g , b , a )
3852 except (ValueError , AttributeError ) as e :
39- logger .warning (f"Invalid color string '{ self .bar_color_str } ': { e } . Using default." )
53+ logger .error (f"Invalid color string '{ self .bar_color_str } ': { e } . Using default." )
4054 return QColor (0 , 255 , 255 , 200 )
4155
4256@dataclass
4357class PositionSettings :
58+ """Overlay window position settings."""
4459 x : int = 0
4560 y : int = 0
4661
4762 def validate (self ) -> bool :
48- """Validate position values are within reasonable bounds.
63+ """Validate position values are within screen bounds.
64+
65+ Uses actual screen size when available, falls back to reasonable defaults.
4966
5067 Returns:
5168 True if all values were valid, False if any were clamped.
5269 """
53- MIN_POS = - 10000
54- MAX_POS = 10000
70+ # Get screen bounds for validation
71+ try :
72+ screen = QApplication .primaryScreen ()
73+ if screen :
74+ size = screen .size ()
75+ MIN_X = - size .width () // 2
76+ MIN_Y = - size .height () // 2
77+ MAX_X = size .width () * 2
78+ MAX_Y = size .height () * 2
79+ else :
80+ # Fallback bounds for headless or error cases
81+ MIN_X = - 10000
82+ MIN_Y = - 10000
83+ MAX_X = 10000
84+ MAX_Y = 10000
85+ except Exception :
86+ # If screen detection fails, use safe defaults
87+ MIN_X = - 10000
88+ MIN_Y = - 10000
89+ MAX_X = 10000
90+ MAX_Y = 10000
91+
5592 valid = True
5693
57- if not (MIN_POS <= self .x <= MAX_POS ):
58- logger .warning (f"X position { self .x } out of range, clamping to [ { MIN_POS } , { MAX_POS } ] " )
59- self .x = max (MIN_POS , min (MAX_POS , self .x ))
94+ if not (MIN_X <= self .x <= MAX_X ):
95+ logger .warning (f"X position { self .x } out of range [ { MIN_X } , { MAX_X } ], clamping " )
96+ self .x = max (MIN_X , min (MAX_X , self .x ))
6097 valid = False
6198
62- if not (MIN_POS <= self .y <= MAX_POS ):
63- logger .warning (f"Y position { self .y } out of range, clamping to [ { MIN_POS } , { MAX_POS } ] " )
64- self .y = max (MIN_POS , min (MAX_POS , self .y ))
99+ if not (MIN_Y <= self .y <= MAX_Y ):
100+ logger .warning (f"Y position { self .y } out of range [ { MIN_Y } , { MAX_Y } ], clamping " )
101+ self .y = max (MIN_Y , min (MAX_Y , self .y ))
65102 valid = False
66103
67104 return valid
68105
69106@dataclass
70107class KeyViewerSettings :
108+ """KeyViewer panel display settings."""
71109 enabled : bool = True
72110 layout : str = "horizontal"
73111 panel_position : str = "below"
@@ -107,30 +145,69 @@ def validate(self) -> bool:
107145
108146@dataclass
109147class AppConfig :
148+ """Main application configuration container.
149+
150+ Attributes:
151+ visual: Visual display settings
152+ position: Window position settings
153+ key_viewer: KeyViewer panel settings
154+ lane_map: Mapping from key strings to lane indices
155+ MAX_BARS: Maximum number of concurrent bars to render
156+ INPUT_LATENCY_OFFSET: Timing offset for input latency compensation
157+ DEBUG_MODE: Enable debug rendering and logging
158+ VERSION: Application version string
159+ CONFIG_VERSION: Config schema version for migrations
160+ display: Display and layout constants
161+ """
110162 visual : VisualSettings = field (default_factory = VisualSettings )
111163 position : PositionSettings = field (default_factory = PositionSettings )
112164 key_viewer : KeyViewerSettings = field (default_factory = KeyViewerSettings )
113- lane_map : Dict [str , int ] = field (default_factory = dict )
114-
165+ lane_map : Dict [str , int ] = field (default_factory = lambda : { 'd' : 0 , 'f' : 1 , 'j' : 2 , 'k' : 3 } )
166+
115167 # Constants
116168 MAX_BARS : int = 300
117169 INPUT_LATENCY_OFFSET : float = 0.0
118- FADE_START_Y : int = 800
119- FADE_RANGE : int = 200
120170 DEBUG_MODE : bool = False
121- VERSION : str = "1.3.7"
122-
123- def __post_init__ (self ):
124- if not self .lane_map :
125- self .lane_map = {'d' : 0 , 'f' : 1 , 'j' : 2 , 'k' : 3 }
171+ VERSION : str = "1.3.9"
172+ CONFIG_VERSION : int = 1 # Config schema version for migrations
173+ display : DisplayConstants = field (default_factory = DisplayConstants )
126174
127175 def set_lane_keys (self , keys : List [str ]) -> None :
128176 """Set lane key mapping.
129177
130178 Args:
131179 keys: List of key strings to map to lanes (by index).
180+
181+ Example:
182+ set_lane_keys(['d', 'f', 'j', 'k']) maps 'd' to lane 0, 'f' to lane 1, etc.
132183 """
133184 new_map = {}
134185 for idx , key in enumerate (keys ):
135186 new_map [key ] = idx
136187 self .lane_map = new_map
188+
189+ def migrate_config (self , from_version : int ) -> None :
190+ """Migrate configuration from an older version.
191+
192+ Args:
193+ from_version: The config version to migrate from.
194+
195+ Example:
196+ migrate_config(0) # Migrate from version 0 to current version
197+ """
198+ if from_version >= self .CONFIG_VERSION :
199+ return # Already at or past current version
200+
201+ # Migration from version 0 to 1
202+ if from_version == 0 :
203+ # Add any new settings introduced in version 1
204+ logger .info (f"Migrating config from version { from_version } to { self .CONFIG_VERSION } " )
205+ # No changes needed for v0->v1 migration
206+
207+ # Future migrations would go here
208+ # Example:
209+ # if from_version == 1:
210+ # # Migrate v1 to v2
211+ # pass
212+
213+ logger .info (f"Config migration complete, now at version { self .CONFIG_VERSION } " )
0 commit comments