Skip to content

Commit efb4327

Browse files
unhappychoiceclaude
andcommitted
feat: implement core Screen trait architecture
Add foundational components for the new screen management system: - Screen trait: Unified interface for all game screens with init, update, render, and event handling methods - ScreenTransitionManager: Manages screen transitions and state changes - GameData: Global game state management with thread-safe access - SessionManager: Handles game session lifecycle and state - StageRepository: Manages game stages and difficulty levels These components provide the foundation for the refactored screen system with better separation of concerns and unified rendering backends. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3986365 commit efb4327

File tree

5 files changed

+1715
-0
lines changed

5 files changed

+1715
-0
lines changed

src/game/game_data.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
use crate::extractor::ExtractionOptions;
2+
use crate::models::{Challenge, GitRepository};
3+
use crate::Result;
4+
use std::path::PathBuf;
5+
use std::sync::{Arc, Mutex, OnceLock};
6+
7+
/// Global game data that persists across screens
8+
#[derive(Debug, Default)]
9+
pub struct GameData {
10+
pub challenges: Option<Vec<Challenge>>,
11+
pub git_repository: Option<GitRepository>,
12+
pub loading_completed: bool,
13+
pub loading_failed: bool,
14+
pub error_message: Option<String>,
15+
// Processing parameters
16+
pub repo_spec: Option<String>,
17+
pub repo_path: Option<PathBuf>,
18+
pub extraction_options: Option<ExtractionOptions>,
19+
}
20+
21+
static GLOBAL_GAME_DATA: OnceLock<Arc<Mutex<GameData>>> = OnceLock::new();
22+
23+
impl GameData {
24+
/// Initialize the global game data instance
25+
pub fn initialize() -> Result<()> {
26+
let game_data = Arc::new(Mutex::new(GameData::default()));
27+
GLOBAL_GAME_DATA.set(game_data).map_err(|_| {
28+
crate::GitTypeError::TerminalError("GameData already initialized".to_string())
29+
})?;
30+
Ok(())
31+
}
32+
33+
/// Get a reference to the global game data instance
34+
pub fn instance() -> Arc<Mutex<GameData>> {
35+
GLOBAL_GAME_DATA
36+
.get()
37+
.expect("GameData not initialized")
38+
.clone()
39+
}
40+
41+
/// Reset the global game data
42+
pub fn reset() -> Result<()> {
43+
if let Some(game_data) = GLOBAL_GAME_DATA.get() {
44+
let mut data = game_data.lock().unwrap();
45+
*data = GameData::default();
46+
}
47+
Ok(())
48+
}
49+
50+
/// Set processing parameters
51+
pub fn set_processing_parameters(
52+
repo_spec: Option<&str>,
53+
repo_path: Option<&PathBuf>,
54+
extraction_options: &ExtractionOptions,
55+
) -> Result<()> {
56+
let game_data = Self::instance();
57+
let mut data = game_data.lock().unwrap();
58+
data.repo_spec = repo_spec.map(|s| s.to_string());
59+
data.repo_path = repo_path.cloned();
60+
data.extraction_options = Some(extraction_options.clone());
61+
Ok(())
62+
}
63+
64+
/// Get processing parameters
65+
pub fn get_processing_parameters(
66+
) -> Option<(Option<String>, Option<PathBuf>, ExtractionOptions)> {
67+
if let Some(game_data) = GLOBAL_GAME_DATA.get() {
68+
let data = game_data.lock().unwrap();
69+
data.extraction_options.as_ref().map(|options| {
70+
(
71+
data.repo_spec.clone(),
72+
data.repo_path.clone(),
73+
options.clone(),
74+
)
75+
})
76+
} else {
77+
None
78+
}
79+
}
80+
81+
/// Set the processing results
82+
pub fn set_results(
83+
challenges: Vec<Challenge>,
84+
git_repository: Option<GitRepository>,
85+
) -> Result<()> {
86+
let game_data = Self::instance();
87+
let mut data = game_data.lock().unwrap();
88+
data.challenges = Some(challenges);
89+
data.git_repository = git_repository;
90+
data.loading_completed = true;
91+
92+
Ok(())
93+
}
94+
95+
/// Set loading failure
96+
pub fn set_loading_failed(error_message: String) -> Result<()> {
97+
let game_data = Self::instance();
98+
let mut data = game_data.lock().unwrap();
99+
data.loading_failed = true;
100+
data.error_message = Some(error_message);
101+
Ok(())
102+
}
103+
104+
/// Check if loading is completed
105+
pub fn is_loading_completed() -> bool {
106+
if let Some(game_data) = GLOBAL_GAME_DATA.get() {
107+
let data = game_data.lock().unwrap();
108+
data.loading_completed
109+
} else {
110+
false
111+
}
112+
}
113+
114+
/// Check if loading failed
115+
pub fn is_loading_failed() -> bool {
116+
if let Some(game_data) = GLOBAL_GAME_DATA.get() {
117+
let data = game_data.lock().unwrap();
118+
data.loading_failed
119+
} else {
120+
false
121+
}
122+
}
123+
124+
/// Get reference to challenges with callback (to avoid lifetime issues)
125+
pub fn with_challenges<F, R>(f: F) -> Option<R>
126+
where
127+
F: FnOnce(&Vec<Challenge>) -> R,
128+
{
129+
if let Some(game_data) = GLOBAL_GAME_DATA.get() {
130+
let data = game_data.lock().unwrap();
131+
data.challenges.as_ref().map(f)
132+
} else {
133+
None
134+
}
135+
}
136+
137+
/// Take the challenges (move out of GameData)
138+
pub fn take_challenges() -> Option<Vec<Challenge>> {
139+
if let Some(game_data) = GLOBAL_GAME_DATA.get() {
140+
let mut data = game_data.lock().unwrap();
141+
data.challenges.take()
142+
} else {
143+
None
144+
}
145+
}
146+
147+
/// Get the git repository if available
148+
pub fn get_git_repository() -> Option<GitRepository> {
149+
if let Some(game_data) = GLOBAL_GAME_DATA.get() {
150+
let data = game_data.lock().unwrap();
151+
data.git_repository.clone()
152+
} else {
153+
None
154+
}
155+
}
156+
157+
/// Set git repository information directly
158+
pub fn set_git_repository(git_repository: Option<GitRepository>) -> Result<()> {
159+
if let Some(game_data) = GLOBAL_GAME_DATA.get() {
160+
let mut data = game_data.lock().unwrap();
161+
data.git_repository = git_repository;
162+
Ok(())
163+
} else {
164+
Err(crate::GitTypeError::TerminalError(
165+
"GameData not initialized".to_string(),
166+
))
167+
}
168+
}
169+
}

src/game/models/screen.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use crate::Result;
2+
use crossterm::event::KeyEvent;
3+
use std::io::Stdout;
4+
use std::time::Duration;
5+
6+
/// Screen type identifiers for different application screens
7+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8+
pub enum ScreenType {
9+
Title,
10+
Loading,
11+
Typing,
12+
StageSummary,
13+
SessionSummary,
14+
TotalSummary,
15+
TotalSummaryShare,
16+
SessionFailure,
17+
History,
18+
Analytics,
19+
SessionDetail,
20+
SessionSharing,
21+
Animation,
22+
VersionCheck,
23+
InfoDialog,
24+
DetailsDialog,
25+
}
26+
27+
/// Update strategy defines how and when a screen should be updated and re-rendered
28+
#[derive(Debug, Clone)]
29+
pub enum UpdateStrategy {
30+
/// Screen only updates when user provides input
31+
InputOnly,
32+
/// Screen updates at regular time intervals
33+
TimeBased(Duration),
34+
/// Screen combines both input and time-based updates
35+
Hybrid {
36+
/// Time interval for automatic updates
37+
interval: Duration,
38+
/// Whether input events should trigger immediate updates
39+
input_priority: bool,
40+
},
41+
}
42+
43+
/// Screen transition actions that can be returned from input handling
44+
#[derive(Debug, Clone)]
45+
pub enum ScreenTransition {
46+
/// No transition - stay on current screen
47+
None,
48+
/// Push new screen onto the stack
49+
Push(ScreenType),
50+
/// Pop current screen from stack
51+
Pop,
52+
/// Replace current screen with new screen
53+
Replace(ScreenType),
54+
/// Pop screens until reaching the specified screen type
55+
PopTo(ScreenType),
56+
/// Exit the application
57+
Exit,
58+
}
59+
60+
/// The Screen trait defines the interface that all screens must implement
61+
pub trait Screen: Send {
62+
/// Initialize the screen - called when screen becomes active
63+
fn init(&mut self) -> Result<()> {
64+
Ok(())
65+
}
66+
67+
/// Handle keyboard input events and return appropriate screen transition
68+
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<ScreenTransition>;
69+
70+
/// Render the screen with access to shared data
71+
fn render_crossterm_with_data(
72+
&mut self,
73+
stdout: &mut Stdout,
74+
session_result: Option<&crate::models::SessionResult>,
75+
total_result: Option<&crate::scoring::TotalResult>,
76+
) -> Result<()>;
77+
78+
/// Render the screen using ratatui backend (optional)
79+
fn render_ratatui(&mut self, _frame: &mut ratatui::Frame) -> Result<()> {
80+
// Default implementation for backward compatibility
81+
// Individual screens can override this when ratatui support is needed
82+
Ok(())
83+
}
84+
85+
/// Clean up screen resources - called when screen becomes inactive
86+
fn cleanup(&mut self) -> Result<()> {
87+
Ok(())
88+
}
89+
90+
/// Get the update strategy for this screen
91+
fn get_update_strategy(&self) -> UpdateStrategy {
92+
UpdateStrategy::InputOnly
93+
}
94+
95+
/// Update screen state and return whether a re-render is needed
96+
fn update(&mut self) -> Result<bool> {
97+
Ok(false)
98+
}
99+
100+
/// Downcast to Any for type-specific access (read-only)
101+
fn as_any(&self) -> &dyn std::any::Any;
102+
103+
/// Downcast to Any for type-specific access (mutable)
104+
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
105+
}

0 commit comments

Comments
 (0)