mod app;
mod config;
mod history;
mod ui;
use crate::{app::App, config::AppConfig, ui::draw};
use anyhow::Result;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::prelude::*;
use std::io;
use std::env;
use std::fs;
use dirs::config_dir;
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
match args[1].as_str() {
"--gen-config" => {
if let Some(mut path) = config_dir() {
path.push("qst");
if fs::create_dir_all(&path).is_err() {
eprintln!("Error: Unable to create configuration directory: {:?}", path);
std::process::exit(1);
}
path.push("config.toml");
if path.exists() {
eprintln!("Error: Configuration file already exists at {:?}", path);
std::process::exit(1);
}
let default_config_struct = AppConfig::default();
match toml::to_string_pretty(&default_config_struct) {
Ok(serialized) => {
match fs::write(&path, serialized) {
Ok(_) => {
println!("Successfully generated default configuration at {:?}", path);
std::process::exit(0);
}
Err(e) => {
eprintln!("Error writing configuration file: {}", e);
std::process::exit(1);
}
}
}
Err(e) => {
eprintln!("Error serializing default configuration: {}", e);
std::process::exit(1);
}
}
} else {
eprintln!("Error: Could not determine configuration directory.");
std::process::exit(1);
}
}
"-h" | "--help" => {
println!("Qst - An Application Launcher");
println!("Usage: qst [OPTIONS]");
println!("");
println!("Options:");
println!(" --gen-config Generate a default config file at ~/.config/qst/config.toml");
println!(" (Fails if file already exists)");
println!(" -h, --help Print this help message");
std::process::exit(0);
}
_ => {
}
}
}
let load_result = AppConfig::load();
if let Some(warning) = &load_result.warning {
eprintln!("{warning}");
}
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(load_result.config, load_result.warning);
loop {
terminal.draw(|f| draw(f, &mut app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
if matches_key(&key, app.config.general.jump_to_top_key.as_deref().unwrap_or("alt+up")) {
app.select_first();
continue;
}
if matches_key(&key, app.config.general.jump_to_bottom_key.as_deref().unwrap_or("alt+down")) {
app.select_last();
continue;
}
match key.code {
KeyCode::Esc => app.should_quit = true,
KeyCode::Enter => app.launch_selected(),
KeyCode::Up => app.move_selection(-1),
KeyCode::Down => app.move_selection(1),
KeyCode::Left => app.move_search_cursor_left(),
KeyCode::Right => app.move_search_cursor_right(),
_ if matches_key(&key, app.config.general.favorite_key.as_deref().unwrap_or("alt+f")) => {
app.toggle_favorite();
}
KeyCode::Backspace => app.backspace_search_char(),
KeyCode::Char(c) => app.insert_search_char(c),
KeyCode::Tab => app.auto_complete(),
_ => {}
}
}
}
if app.should_quit {
break;
}
}
disable_raw_mode()?;
execute!(io::stdout(), LeaveAlternateScreen)?;
Ok(())
}
fn matches_key(key: &event::KeyEvent, config_str: &str) -> bool {
let config_str = config_str.to_lowercase();
let parts: Vec<&str> = config_str.split('+').collect();
let mut required_modifiers = KeyModifiers::empty();
let mut required_code = None;
for part in parts {
match part {
"ctrl" | "control" => required_modifiers.insert(KeyModifiers::CONTROL),
"alt" | "option" => required_modifiers.insert(KeyModifiers::ALT),
"shift" => required_modifiers.insert(KeyModifiers::SHIFT),
"super" | "cmd" | "win" | "meta" => required_modifiers.insert(KeyModifiers::SUPER),
"enter" | "return" => required_code = Some(KeyCode::Enter),
"esc" | "escape" => required_code = Some(KeyCode::Esc),
"backspace" => required_code = Some(KeyCode::Backspace),
"tab" => required_code = Some(KeyCode::Tab),
"space" => required_code = Some(KeyCode::Char(' ')),
"up" => required_code = Some(KeyCode::Up),
"down" => required_code = Some(KeyCode::Down),
"left" => required_code = Some(KeyCode::Left),
"right" => required_code = Some(KeyCode::Right),
s if s.len() == 1 => required_code = Some(KeyCode::Char(s.chars().next().unwrap())),
s if s.starts_with('f') && s.len() > 1 => {
if let Ok(n) = s[1..].parse::<u8>() {
required_code = Some(KeyCode::F(n));
}
}
_ => {}
}
}
if let Some(code) = required_code {
if key.code != code {
return false;
}
}
key.modifiers.contains(required_modifiers)
}