Skip to content

Commit 87b2ccd

Browse files
unhappychoiceclaude
andcommitted
feat: add Console abstraction for standard I/O
Abstract standard input/output to infrastructure layer. Replace direct println!, eprintln!, io::stdin(), io::stdout() usage in CLI commands with Console trait. Implementation: - RealConsole: uses actual standard I/O (println!, eprintln!, etc.) - MockConsole: stores output in vectors for testing (test-mocks feature) Console trait methods: - print, println, eprintln: output methods - read_line: input method - flush: flush stdout Files updated: - All CLI command files (repo, game, stats, history, export, trending) - Replace all standard I/O calls with Console abstraction 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f5b10b4 commit 87b2ccd

File tree

7 files changed

+220
-58
lines changed

7 files changed

+220
-58
lines changed

src/infrastructure/console.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use crate::Result;
2+
3+
/// Trait for console I/O abstraction
4+
pub trait Console {
5+
fn print(&self, message: &str) -> Result<()>;
6+
fn println(&self, message: &str) -> Result<()>;
7+
fn eprintln(&self, message: &str) -> Result<()>;
8+
fn read_line(&self, buffer: &mut String) -> Result<()>;
9+
fn flush(&self) -> Result<()>;
10+
}
11+
12+
#[cfg(not(feature = "test-mocks"))]
13+
mod real_impl {
14+
use super::*;
15+
use std::io::{self, Write};
16+
17+
#[derive(Debug, Clone)]
18+
pub struct RealConsole;
19+
20+
impl RealConsole {
21+
pub fn new() -> Self {
22+
Self
23+
}
24+
}
25+
26+
impl Default for RealConsole {
27+
fn default() -> Self {
28+
Self::new()
29+
}
30+
}
31+
32+
impl Console for RealConsole {
33+
fn print(&self, message: &str) -> Result<()> {
34+
print!("{}", message);
35+
Ok(())
36+
}
37+
38+
fn println(&self, message: &str) -> Result<()> {
39+
println!("{}", message);
40+
Ok(())
41+
}
42+
43+
fn eprintln(&self, message: &str) -> Result<()> {
44+
eprintln!("{}", message);
45+
Ok(())
46+
}
47+
48+
fn read_line(&self, buffer: &mut String) -> Result<()> {
49+
io::stdin().read_line(buffer)?;
50+
Ok(())
51+
}
52+
53+
fn flush(&self) -> Result<()> {
54+
io::stdout().flush()?;
55+
Ok(())
56+
}
57+
}
58+
}
59+
60+
#[cfg(feature = "test-mocks")]
61+
mod mock_impl {
62+
use super::*;
63+
use std::cell::RefCell;
64+
65+
#[derive(Debug, Clone)]
66+
pub struct MockConsole {
67+
pub output: RefCell<Vec<String>>,
68+
pub error_output: RefCell<Vec<String>>,
69+
pub input_lines: RefCell<Vec<String>>,
70+
}
71+
72+
impl MockConsole {
73+
pub fn new() -> Self {
74+
Self {
75+
output: RefCell::new(Vec::new()),
76+
error_output: RefCell::new(Vec::new()),
77+
input_lines: RefCell::new(Vec::new()),
78+
}
79+
}
80+
81+
pub fn set_input_lines(&self, lines: Vec<String>) {
82+
*self.input_lines.borrow_mut() = lines;
83+
}
84+
85+
pub fn get_output(&self) -> Vec<String> {
86+
self.output.borrow().clone()
87+
}
88+
89+
pub fn get_error_output(&self) -> Vec<String> {
90+
self.error_output.borrow().clone()
91+
}
92+
}
93+
94+
impl Default for MockConsole {
95+
fn default() -> Self {
96+
Self::new()
97+
}
98+
}
99+
100+
impl Console for MockConsole {
101+
fn print(&self, message: &str) -> Result<()> {
102+
self.output.borrow_mut().push(message.to_string());
103+
Ok(())
104+
}
105+
106+
fn println(&self, message: &str) -> Result<()> {
107+
self.output.borrow_mut().push(message.to_string());
108+
Ok(())
109+
}
110+
111+
fn eprintln(&self, message: &str) -> Result<()> {
112+
self.error_output.borrow_mut().push(message.to_string());
113+
Ok(())
114+
}
115+
116+
fn read_line(&self, buffer: &mut String) -> Result<()> {
117+
let mut input_lines = self.input_lines.borrow_mut();
118+
if let Some(line) = input_lines.first() {
119+
buffer.push_str(line);
120+
if !line.ends_with('\n') {
121+
buffer.push('\n');
122+
}
123+
input_lines.remove(0);
124+
}
125+
Ok(())
126+
}
127+
128+
fn flush(&self) -> Result<()> {
129+
Ok(())
130+
}
131+
}
132+
}
133+
134+
#[cfg(not(feature = "test-mocks"))]
135+
pub use real_impl::RealConsole as ConsoleImpl;
136+
137+
#[cfg(feature = "test-mocks")]
138+
pub use mock_impl::MockConsole as ConsoleImpl;

src/infrastructure/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod browser;
2+
pub mod console;
23
pub mod database;
34
pub mod git;
45
pub mod http;
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
use crate::infrastructure::console::{Console, ConsoleImpl};
12
use crate::Result;
23
use std::path::PathBuf;
34

45
pub fn run_export(format: String, output: Option<PathBuf>) -> Result<()> {
5-
eprintln!("❌ Export command is not yet implemented");
6-
eprintln!("💡 Requested format: {}", format);
6+
let console = ConsoleImpl::new();
7+
console.eprintln("❌ Export command is not yet implemented")?;
8+
console.eprintln(&format!("💡 Requested format: {}", format))?;
79
if let Some(path) = output {
8-
eprintln!("💡 Requested output: {}", path.display());
10+
console.eprintln(&format!("💡 Requested output: {}", path.display()))?;
911
}
10-
eprintln!("💡 This feature is planned for a future release");
12+
console.eprintln("💡 This feature is planned for a future release")?;
1113
std::process::exit(1);
1214
}

src/presentation/cli/commands/game.rs

Lines changed: 58 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::domain::models::ExtractionOptions;
33
use crate::domain::models::Languages;
44
use crate::domain::services::theme_manager::ThemeManager;
55
use crate::domain::services::version_service::VersionService;
6+
use crate::infrastructure::console::{Console, ConsoleImpl};
67
use crate::infrastructure::logging;
78
use crate::presentation::cli::args::Cli;
89
use crate::presentation::game::{GameData, SessionManager};
@@ -16,6 +17,8 @@ use std::sync::{Arc, Mutex};
1617
pub fn run_game_session(cli: Cli) -> Result<()> {
1718
log::info!("Starting GitType game session");
1819

20+
let console = ConsoleImpl::new();
21+
1922
// Create single EventBus instance for the entire application
2023
let event_bus = EventBus::new();
2124

@@ -47,8 +50,11 @@ pub fn run_game_session(cli: Cli) -> Result<()> {
4750
// Initialize theme manager
4851
if let Err(e) = ThemeManager::init() {
4952
log::warn!("Failed to initialize theme manager: {}", e);
50-
eprintln!("⚠️ Warning: Failed to load theme configuration: {}", e);
51-
eprintln!(" Using default theme.");
53+
console.eprintln(&format!(
54+
"⚠️ Warning: Failed to load theme configuration: {}",
55+
e
56+
))?;
57+
console.eprintln(" Using default theme.")?;
5258
}
5359

5460
// Session repository will be initialized in DatabaseInitStep during loading screen
@@ -57,16 +63,16 @@ pub fn run_game_session(cli: Cli) -> Result<()> {
5763

5864
if let Some(langs) = cli.langs {
5965
if let Err(unsupported_langs) = Languages::validate_languages(&langs) {
60-
eprintln!(
66+
console.eprintln(&format!(
6167
"❌ Unsupported language(s): {}",
6268
unsupported_langs.join(", ")
63-
);
64-
eprintln!("💡 Supported languages:");
69+
))?;
70+
console.eprintln("💡 Supported languages:")?;
6571
let supported = Languages::get_supported_languages();
6672
let mut supported_display = supported.clone();
6773
supported_display.dedup();
6874
for chunk in supported_display.chunks(6) {
69-
eprintln!(" {}", chunk.join(", "));
75+
console.eprintln(&format!(" {}", chunk.join(", ")))?;
7076
}
7177
std::process::exit(1);
7278
}
@@ -127,108 +133,113 @@ pub fn run_game_session(cli: Cli) -> Result<()> {
127133
}
128134
Err(e) => {
129135
log::error!("Game session failed with error: {}", e);
130-
handle_game_error(e)?;
136+
handle_game_error(&console, e)?;
131137
}
132138
}
133139

134140
Ok(())
135141
}
136142

137-
fn handle_game_error(e: GitTypeError) -> Result<()> {
143+
fn handle_game_error(console: &impl Console, e: GitTypeError) -> Result<()> {
138144
// Log the error details for debugging before handling user-friendly output
139145
logging::log_error_to_file(&e);
140146

141147
match e {
142148
GitTypeError::NoSupportedFiles => {
143-
eprintln!("❌ No code chunks found in the repository");
144-
eprintln!("💡 Try:");
145-
eprintln!(" • Using a different repository path");
146-
eprintln!(" • Adjusting --langs filter (e.g., --langs rust,python)");
149+
console.eprintln("❌ No code chunks found in the repository")?;
150+
console.eprintln("💡 Try:")?;
151+
console.eprintln(" • Using a different repository path")?;
152+
console.eprintln(" • Adjusting --langs filter (e.g., --langs rust,python)")?;
147153
std::process::exit(1);
148154
}
149155
GitTypeError::RepositoryNotFound(path) => {
150-
eprintln!("❌ Repository not found at path: {}", path.display());
151-
eprintln!("💡 Ensure the path exists and is a valid repository");
156+
console.eprintln(&format!(
157+
"❌ Repository not found at path: {}",
158+
path.display()
159+
))?;
160+
console.eprintln("💡 Ensure the path exists and is a valid repository")?;
152161
std::process::exit(1);
153162
}
154163
GitTypeError::RepositoryCloneError(git_error) => {
155-
eprintln!("❌ Failed to clone repository: {}", git_error);
156-
eprintln!("💡 Check:");
157-
eprintln!(" • Repository URL is correct");
158-
eprintln!(" • You have access to the repository");
159-
eprintln!(" • Internet connection is available");
164+
console.eprintln(&format!("❌ Failed to clone repository: {}", git_error))?;
165+
console.eprintln("💡 Check:")?;
166+
console.eprintln(" • Repository URL is correct")?;
167+
console.eprintln(" • You have access to the repository")?;
168+
console.eprintln(" • Internet connection is available")?;
160169
std::process::exit(1);
161170
}
162171
GitTypeError::ExtractionFailed(msg) => {
163-
eprintln!("❌ Code extraction failed: {}", msg);
164-
eprintln!("💡 Try using different --langs filter");
172+
console.eprintln(&format!("❌ Code extraction failed: {}", msg))?;
173+
console.eprintln("💡 Try using different --langs filter")?;
165174
std::process::exit(1);
166175
}
167176
GitTypeError::InvalidRepositoryFormat(msg) => {
168-
eprintln!("❌ Invalid repository format: {}", msg);
169-
eprintln!("💡 Supported formats:");
170-
eprintln!(" • owner/repo");
171-
eprintln!(" • https://github.com/owner/repo");
172-
eprintln!(" • git@github.com:owner/repo.git");
177+
console.eprintln(&format!("❌ Invalid repository format: {}", msg))?;
178+
console.eprintln("💡 Supported formats:")?;
179+
console.eprintln(" • owner/repo")?;
180+
console.eprintln(" • https://github.com/owner/repo")?;
181+
console.eprintln(" • git@github.com:owner/repo.git")?;
173182
std::process::exit(1);
174183
}
175184
GitTypeError::IoError(io_error) => {
176-
eprintln!("❌ IO error: {}", io_error);
185+
console.eprintln(&format!("❌ IO error: {}", io_error))?;
177186
std::process::exit(1);
178187
}
179188
GitTypeError::DatabaseError(db_error) => {
180-
eprintln!("❌ Database error: {}", db_error);
189+
console.eprintln(&format!("❌ Database error: {}", db_error))?;
181190
std::process::exit(1);
182191
}
183192
GitTypeError::GlobPatternError(glob_error) => {
184-
eprintln!("❌ Invalid glob pattern: {}", glob_error);
185-
eprintln!("💡 Check your glob patterns in ExtractionOptions");
193+
console.eprintln(&format!("❌ Invalid glob pattern: {}", glob_error))?;
194+
console.eprintln("💡 Check your glob patterns in ExtractionOptions")?;
186195
std::process::exit(1);
187196
}
188197
GitTypeError::SerializationError(json_error) => {
189-
eprintln!("❌ Serialization error: {}", json_error);
198+
console.eprintln(&format!("❌ Serialization error: {}", json_error))?;
190199
std::process::exit(1);
191200
}
192201
GitTypeError::TerminalError(msg) => {
193-
eprintln!("❌ Terminal error: {}", msg);
202+
console.eprintln(&format!("❌ Terminal error: {}", msg))?;
194203
if msg.contains("No such device or address") {
195-
eprintln!("💡 This error often occurs in WSL or SSH environments where terminal features are limited.");
196-
eprintln!(" Try running GitType in a native terminal or GUI terminal emulator.");
204+
console.eprintln("💡 This error often occurs in WSL or SSH environments where terminal features are limited.")?;
205+
console.eprintln(
206+
" Try running GitType in a native terminal or GUI terminal emulator.",
207+
)?;
197208
}
198209
std::process::exit(1);
199210
}
200211
GitTypeError::WalkDirError(walk_error) => {
201-
eprintln!("❌ Directory walk error: {}", walk_error);
202-
eprintln!("💡 Check directory permissions and try again");
212+
console.eprintln(&format!("❌ Directory walk error: {}", walk_error))?;
213+
console.eprintln("💡 Check directory permissions and try again")?;
203214
std::process::exit(1);
204215
}
205216
GitTypeError::TreeSitterLanguageError(lang_error) => {
206-
eprintln!("❌ Language parsing error: {}", lang_error);
207-
eprintln!("💡 This might be caused by unsupported language features");
217+
console.eprintln(&format!("❌ Language parsing error: {}", lang_error))?;
218+
console.eprintln("💡 This might be caused by unsupported language features")?;
208219
std::process::exit(1);
209220
}
210221
GitTypeError::PanicError(msg) => {
211-
eprintln!("💥 Application panic occurred: {}", msg);
212-
eprintln!("💡 This indicates an unexpected error. Please report this issue.");
222+
console.eprintln(&format!("💥 Application panic occurred: {}", msg))?;
223+
console.eprintln("💡 This indicates an unexpected error. Please report this issue.")?;
213224
std::process::exit(1);
214225
}
215226
GitTypeError::HttpError(http_error) => {
216-
eprintln!("❌ HTTP request failed: {}", http_error);
217-
eprintln!("💡 Check your internet connection and try again");
227+
console.eprintln(&format!("❌ HTTP request failed: {}", http_error))?;
228+
console.eprintln("💡 Check your internet connection and try again")?;
218229
std::process::exit(1);
219230
}
220231
GitTypeError::ApiError(msg) => {
221-
eprintln!("❌ API error: {}", msg);
222-
eprintln!("💡 The service may be temporarily unavailable");
232+
console.eprintln(&format!("❌ API error: {}", msg))?;
233+
console.eprintln("💡 The service may be temporarily unavailable")?;
223234
std::process::exit(1);
224235
}
225236
GitTypeError::ValidationError(msg) => {
226-
eprintln!("❌ {}", msg);
237+
console.eprintln(&format!("❌ {}", msg))?;
227238
std::process::exit(1);
228239
}
229240
GitTypeError::ScreenInitializationError(msg) => {
230-
eprintln!("❌ Screen initialization error: {}", msg);
231-
eprintln!("💡 This is an internal error. Please report this issue.");
241+
console.eprintln(&format!("❌ Screen initialization error: {}", msg))?;
242+
console.eprintln("💡 This is an internal error. Please report this issue.")?;
232243
std::process::exit(1);
233244
}
234245
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use crate::infrastructure::console::{Console, ConsoleImpl};
12
use crate::Result;
23

34
pub fn run_history() -> Result<()> {
4-
eprintln!("❌ History command is not yet implemented");
5-
eprintln!("💡 This feature is planned for a future release");
5+
let console = ConsoleImpl::new();
6+
console.eprintln("❌ History command is not yet implemented")?;
7+
console.eprintln("💡 This feature is planned for a future release")?;
68
std::process::exit(1);
79
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use crate::infrastructure::console::{Console, ConsoleImpl};
12
use crate::Result;
23

34
pub fn run_stats() -> Result<()> {
4-
eprintln!("❌ Stats command is not yet implemented");
5-
eprintln!("💡 This feature is planned for a future release");
5+
let console = ConsoleImpl::new();
6+
console.eprintln("❌ Stats command is not yet implemented")?;
7+
console.eprintln("💡 This feature is planned for a future release")?;
68
std::process::exit(1);
79
}

0 commit comments

Comments
 (0)