sticks 0.3.5

A tool for managing C and C++ projects
Documentation
use crate::{BuildSystem, Language};
use anyhow::Result;
use std::io::{self, Read, Write};

pub fn run_interactive() -> Result<()> {
	println!("\n🎯 Welcome to sticks - Interactive Mode\n");

	print!("📝 Enter project name: ");
	io::stdout().flush()?;
	let mut project_name = String::new();
	io::stdin().read_line(&mut project_name)?;
	let project_name = project_name.trim().to_string();

	if project_name.is_empty() {
		anyhow::bail!("Project name cannot be empty");
	}

	let language = select_language_interactive()?;
	let build_system = select_build_system_interactive()?;

	println!("\n🔨 Creating {} project: {}", language, project_name);
	crate::new_project_with_system(&project_name, language, build_system)?;
	println!("✅ Project created successfully!\n");

	Ok(())
}

pub fn select_language_interactive() -> Result<Language> {
	let options = vec!["C", "C++"];
	let selected = interactive_select(&options)?;

	Ok(match selected {
		0 => Language::C,
		1 => Language::Cpp,
		_ => unreachable!(),
	})
}

pub fn select_build_system_interactive() -> Result<BuildSystem> {
	let options = vec!["Makefile", "CMake"];
	let selected = interactive_select(&options)?;

	Ok(match selected {
		0 => BuildSystem::Makefile,
		1 => BuildSystem::CMake,
		_ => unreachable!(),
	})
}

fn interactive_select(options: &[&str]) -> Result<usize> {
	let mut selected = 0;
	let num_options = options.len();

	let _guard = RawModeGuard::new()?;

	display_options(options, selected)?;

	loop {
		let input = read_key()?;

		match input.as_str() {
			"UP" => {
				if selected > 0 {
					selected -= 1;
					move_cursor_up(num_options)?;
					display_options(options, selected)?;
				}
			}
			"DOWN" => {
				if selected < num_options - 1 {
					selected += 1;
					move_cursor_up(num_options)?;
					display_options(options, selected)?;
				}
			}
			"ENTER" => {
				move_cursor_up(num_options)?;
				println!("{}", options[selected]);
				break;
			}
			_ => {}
		}
	}

	Ok(selected)
}

fn display_options(options: &[&str], selected: usize) -> Result<()> {
	for (i, option) in options.iter().enumerate() {
		if i == selected {
			println!("{}", option);
		} else {
			println!("  {}", option);
		}
	}
	io::stdout().flush()?;
	Ok(())
}

fn move_cursor_up(lines: usize) -> Result<()> {
	for _ in 0..lines {
		print!("\x1B[A\x1B[2K");
	}
	io::stdout().flush()?;
	Ok(())
}

struct RawModeGuard {
	original_termios: libc::termios,
}

impl RawModeGuard {
	fn new() -> Result<Self> {
		use std::os::unix::io::AsRawFd;

		let stdin_fd = io::stdin().as_raw_fd();
		let original_termios = unsafe {
			let mut t = std::mem::zeroed::<libc::termios>();
			libc::tcgetattr(stdin_fd, &mut t);
			t
		};

		let mut new_termios = original_termios;
		new_termios.c_lflag &= !(libc::ICANON | libc::ECHO);
		new_termios.c_cc[libc::VMIN] = 1;
		new_termios.c_cc[libc::VTIME] = 0;

		unsafe {
			libc::tcsetattr(stdin_fd, libc::TCSADRAIN, &new_termios);
		}

		Ok(RawModeGuard { original_termios })
	}
}

impl Drop for RawModeGuard {
	fn drop(&mut self) {
		use std::os::unix::io::AsRawFd;

		let stdin_fd = io::stdin().as_raw_fd();
		unsafe {
			libc::tcsetattr(stdin_fd, libc::TCSADRAIN, &self.original_termios);
		}
	}
}

fn read_key() -> Result<String> {
	let mut buf = [0; 3];

	let n = io::stdin().read(&mut buf)?;

	if n == 0 {
		return Ok(String::new());
	}

	if n >= 3 && buf[0] == 27 && buf[1] == 91 {
		match buf[2] {
			b'A' => return Ok("UP".to_string()),
			b'B' => return Ok("DOWN".to_string()),
			_ => {}
		}
	}

	if buf[0] == b'\n' || buf[0] == b'\r' {
		return Ok("ENTER".to_string());
	}

	Ok(String::new())
}

pub fn select_language() -> Language {
	match select_language_interactive() {
		Ok(lang) => lang,
		Err(_) => Language::C,
	}
}

pub fn select_build_system() -> Result<BuildSystem> {
	select_build_system_interactive()
}