use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::app::{App, FocusedArea};
use super::widgets::{
self, WidgetKey, popups::confirm as confirm_popup, popups::import_modpack, popups::new_instance,
};
use crate::tui::error_buffer;
impl App {
pub(super) fn handle_key_event(&mut self, key_event: KeyEvent) -> color_eyre::Result<()> {
if self.focused == FocusedArea::OverviewExpanded {
if self.log_overlay_search.active {
match key_event.code {
KeyCode::Enter => {
self.log_overlay_search.confirm();
}
KeyCode::Esc => {
self.log_overlay_search.deactivate();
}
KeyCode::Backspace => {
self.log_overlay_search.pop();
}
KeyCode::Char(c) => {
self.log_overlay_search.push(c);
}
_ => {}
}
return Ok(());
}
match key_event.code {
KeyCode::Char('O') | KeyCode::Esc => {
self.focused = self.pre_overlay_focused;
self.log_overlay_search.deactivate();
return Ok(());
}
KeyCode::Char('j') | KeyCode::Down => {
if self.log_overlay_scroll < self.log_overlay_max_scroll {
self.log_overlay_scroll += 1;
}
return Ok(());
}
KeyCode::Char('k') | KeyCode::Up => {
self.log_overlay_scroll = self.log_overlay_scroll.saturating_sub(1);
return Ok(());
}
KeyCode::Char('G') => {
self.log_overlay_scroll = self.log_overlay_max_scroll;
return Ok(());
}
KeyCode::Char('g') => {
self.log_overlay_scroll = 0;
return Ok(());
}
KeyCode::Char('/') => {
self.log_overlay_search.activate();
return Ok(());
}
_ => {
return Ok(());
}
}
}
if self.focused == FocusedArea::ConfirmDelete {
match key_event.code {
KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => {
let focus_after = match confirm_popup::pending_target() {
Some(confirm_popup::ConfirmTarget::Instance { name }) => {
match self.instance_manager.delete(&name) {
Ok(_) => {
self.instances_state.remove_instance(&name);
}
Err(e) => {
tracing::error!("Failed to delete instance '{}': {}", name, e);
}
}
FocusedArea::Instances
}
Some(confirm_popup::ConfirmTarget::Account { index, .. }) => {
let count = self.account_state.store.accounts.len();
self.account_state.store.remove(index);
if count > 1 {
self.account_state.list_state.selected = Some(index.min(
self.account_state.store.accounts.len().saturating_sub(1),
));
} else {
self.account_state.list_state.selected = None;
}
FocusedArea::Account
}
Some(confirm_popup::ConfirmTarget::ConfigProfile { profile }) => {
if let Err(e) = self.delete_config_profile(&profile) {
error_buffer::push_error(error_buffer::ErrorEvent {
id: 0,
level: tracing::Level::ERROR,
message: e.to_string(),
pushed_at: std::time::Instant::now(),
});
}
FocusedArea::Settings
}
Some(confirm_popup::ConfirmTarget::Content { name, path }) => {
match delete_content_path(&path) {
Ok(()) => {
self.remove_content_path_from_states(&path);
}
Err(e) => {
tracing::error!("Failed to delete content '{}': {}", name, e);
}
}
FocusedArea::Content
}
None => FocusedArea::Instances,
};
confirm_popup::clear_pending();
self.focused = focus_after;
return Ok(());
}
KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => {
let focus_after = match confirm_popup::pending_target() {
Some(confirm_popup::ConfirmTarget::Content { .. }) => FocusedArea::Content,
Some(confirm_popup::ConfirmTarget::Account { .. }) => FocusedArea::Account,
Some(confirm_popup::ConfirmTarget::ConfigProfile { .. }) => {
FocusedArea::Settings
}
_ => FocusedArea::Instances,
};
confirm_popup::clear_pending();
self.focused = focus_after;
return Ok(());
}
_ => {
return Ok(());
}
}
}
if self.focused == FocusedArea::Content {
if self.content_tab == widgets::content::ContentTab::Logs {
if key_event.code == KeyCode::Char('d')
&& !self.logs_state.search.active
&& !self.logs_state.viewer_search.active
{
if let Some(pending) = self.logs_state.pending_delete() {
confirm_popup::set_pending_content_delete(pending.name, pending.path);
self.focused = FocusedArea::ConfirmDelete;
}
return Ok(());
}
if widgets::logs_viewer::handle_key(&key_event, &mut self.logs_state) {
return Ok(());
}
} else if self.content_tab == widgets::content::ContentTab::Screenshots {
if key_event.code == KeyCode::Char('d') && !self.screenshots_state.search.active {
if let Some(pending) = self.screenshots_state.pending_delete() {
confirm_popup::set_pending_content_delete(pending.name, pending.path);
self.focused = FocusedArea::ConfirmDelete;
}
return Ok(());
}
if widgets::screenshots_grid::handle_key(&key_event, &mut self.screenshots_state) {
return Ok(());
}
} else if self.content_tab == widgets::content::ContentTab::Worlds {
if key_event.code == KeyCode::Char('d') && !self.worlds_state.search.active {
if let Some(pending) = self.worlds_state.pending_delete() {
confirm_popup::set_pending_content_delete(pending.name, pending.path);
self.focused = FocusedArea::ConfirmDelete;
}
return Ok(());
}
if widgets::content::list::handle_key_no_toggle(&key_event, &mut self.worlds_state)
{
return Ok(());
}
} else {
let state = match self.content_tab {
widgets::content::ContentTab::Mods => Some(&mut self.mods_state),
widgets::content::ContentTab::ResourcePacks => {
Some(&mut self.resource_packs_state)
}
widgets::content::ContentTab::Shaders => Some(&mut self.shaders_state),
_ => None,
};
if let Some(state) = state {
if key_event.code == KeyCode::Char('d') && !state.search.active {
if let Some(pending) = state.pending_delete() {
confirm_popup::set_pending_content_delete(pending.name, pending.path);
self.focused = FocusedArea::ConfirmDelete;
}
return Ok(());
}
if widgets::content::list::handle_key(&key_event, state) {
return Ok(());
}
}
}
}
if self.focused == FocusedArea::Account
&& let KeyCode::Char('d') = key_event.code
&& let Some(index) = self.account_state.list_state.selected
&& let Some(account) = self.account_state.store.accounts.get(index)
{
confirm_popup::set_pending(confirm_popup::ConfirmTarget::Account {
username: account.username.clone(),
index,
});
self.focused = FocusedArea::ConfirmDelete;
return Ok(());
}
if self.focused == FocusedArea::Account
&& widgets::account::handle_key(&key_event, &mut self.account_state)
{
return Ok(());
}
if self.focused == FocusedArea::Settings {
match widgets::settings::handle_key(
&key_event,
&mut self.settings_state,
self.instances_state.selected_instance(),
&self.instance_manager.instances_dir,
) {
widgets::settings::SettingsAction::EditInstance(path)
| widgets::settings::SettingsAction::EditGlobal(path) => {
self.pending_editor = Some(path);
return Ok(());
}
widgets::settings::SettingsAction::ToggleDesktop => {
if let Some(inst) = self.instances_state.selected_instance() {
let name = inst.name.clone();
match crate::instance::desktop::toggle(inst) {
Ok(true) => {
error_buffer::push_error(error_buffer::ErrorEvent {
id: 0,
level: tracing::Level::INFO,
message: format!("Desktop shortcut created for '{name}'"),
pushed_at: std::time::Instant::now(),
});
}
Ok(false) => {
error_buffer::push_error(error_buffer::ErrorEvent {
id: 0,
level: tracing::Level::INFO,
message: format!("Desktop shortcut removed for '{name}'"),
pushed_at: std::time::Instant::now(),
});
}
Err(e) => {
tracing::error!("Failed to toggle desktop shortcut: {}", e);
}
}
}
return Ok(());
}
widgets::settings::SettingsAction::SelectProfile(profile) => {
if let Some(inst) = self.instances_state.selected_instance().cloned() {
let instance_dir = self.instance_manager.instances_dir.join(&inst.name);
match crate::instance::config_sync::switch_profile(
&inst.name,
inst.config_sync_profile.as_deref(),
profile.as_deref(),
&self.instance_manager.meta_dir,
&instance_dir,
) {
Ok(selected) => {
let mut updated = inst.clone();
updated.config_sync_profile = selected;
if let Err(e) = self.instance_manager.save(&updated) {
tracing::error!("Failed to save config profile: {}", e);
} else {
self.instances_state.replace_instance(&inst.name, updated);
}
}
Err(e) => {
error_buffer::push_error(error_buffer::ErrorEvent {
id: 0,
level: tracing::Level::ERROR,
message: e.to_string(),
pushed_at: std::time::Instant::now(),
});
}
}
}
return Ok(());
}
widgets::settings::SettingsAction::ConfirmDeleteProfile(profile) => {
confirm_popup::set_pending(confirm_popup::ConfirmTarget::ConfigProfile {
profile,
});
self.focused = FocusedArea::ConfirmDelete;
return Ok(());
}
widgets::settings::SettingsAction::Error(message) => {
error_buffer::push_error(error_buffer::ErrorEvent {
id: 0,
level: tracing::Level::ERROR,
message,
pushed_at: std::time::Instant::now(),
});
return Ok(());
}
widgets::settings::SettingsAction::None => {}
}
}
match self.focused {
FocusedArea::Popup => {
new_instance::handle_key(&key_event, &mut self.instances_state);
}
FocusedArea::ImportPopup => {
import_modpack::handle_key(&key_event, &mut self.instances_state);
}
_ => {
if self.focused == FocusedArea::Instances && self.instances_state.renaming.is_some()
{
match key_event.code {
KeyCode::Enter => {
let new_name = self.instances_state.renaming.take().unwrap_or_default();
if let Some(inst) = self.instances_state.selected_instance() {
let old_name = inst.name.clone();
if let Ok(()) = self.instance_manager.rename(&old_name, &new_name)
&& let Some(inst) = self
.instances_state
.instances
.iter_mut()
.find(|i| i.name == old_name)
{
inst.name = new_name.trim().to_owned();
}
}
}
KeyCode::Esc => {
self.instances_state.renaming = None;
}
KeyCode::Backspace => {
if let Some(ref mut name) = self.instances_state.renaming {
name.pop();
}
}
KeyCode::Char(c) => {
if let Some(ref mut name) = self.instances_state.renaming {
name.push(c);
}
}
_ => {}
}
return Ok(());
}
if self.focused == FocusedArea::Instances && self.instances_state.search.active {
self.instances_state.handle_key(&key_event);
return Ok(());
}
match key_event.code {
KeyCode::Char('q') => self.exit = true,
KeyCode::Char('I') => self.focused = FocusedArea::Instances,
KeyCode::Char('C') => self.focused = FocusedArea::Content,
KeyCode::Char('A') => self.focused = FocusedArea::Account,
KeyCode::Char('S') => self.focused = FocusedArea::Settings,
KeyCode::Char('O') => {
self.pre_overlay_focused = self.focused;
self.focused = FocusedArea::OverviewExpanded;
}
KeyCode::Tab | KeyCode::Char('l') | KeyCode::Right
if self.focused == FocusedArea::Content =>
{
self.content_tab = self.content_tab.next();
}
KeyCode::BackTab | KeyCode::Char('h') | KeyCode::Left
if self.focused == FocusedArea::Content =>
{
self.content_tab = self.content_tab.previous();
}
KeyCode::Char('d')
if self.focused == FocusedArea::Instances
&& !self.instances_state.search.active =>
{
if let Some(instance) = self.instances_state.selected_instance() {
let name = instance.name.clone();
confirm_popup::set_pending_instance_delete(&name);
self.focused = FocusedArea::ConfirmDelete;
}
}
KeyCode::Enter
if self.focused == FocusedArea::Instances
&& !self.instances_state.search.active
&& key_event.modifiers.contains(KeyModifiers::SHIFT) =>
{
if let Some(instance) = self.instances_state.selected_instance() {
let dir = self
.instance_manager
.instances_dir
.join(&instance.name)
.join(".minecraft");
if let Err(e) = open::that_detached(&dir) {
tracing::error!("Failed to open instance directory: {}", e);
}
}
}
KeyCode::Enter
if self.focused == FocusedArea::Instances
&& !self.instances_state.search.active =>
{
self.focused = FocusedArea::Content;
}
KeyCode::Char('l')
if self.focused == FocusedArea::Instances
&& !self.instances_state.search.active =>
{
if let Some(instance) = self.instances_state.selected_instance().cloned() {
let can_launch = matches!(
crate::running::get(&instance.name),
None | Some(crate::running::RunState::Crashed(_))
);
if can_launch {
crate::running::remove(&instance.name);
crate::instance_logs::clear(&instance.name);
self.spawn_launch(instance);
}
}
}
KeyCode::Char('r')
if self.focused == FocusedArea::Instances
&& !self.instances_state.search.active =>
{
if let Some(inst) = self.instances_state.selected_instance() {
self.instances_state.renaming = Some(inst.name.clone());
}
}
KeyCode::Esc
if self.focused == FocusedArea::Instances
&& !self.instances_state.search.active =>
{
if let Some(instance) = self.instances_state.selected_instance() {
crate::running::send_kill(&instance.name);
}
}
_ => {}
}
if self.focused == FocusedArea::Instances {
self.instances_state.handle_key(&key_event)
}
}
}
if self.instances_state.wants_popup() {
self.focused = FocusedArea::Popup;
} else if self.focused == FocusedArea::Popup {
self.focused = FocusedArea::Instances;
}
if self.instances_state.wants_import_popup() {
self.focused = FocusedArea::ImportPopup;
} else if self.focused == FocusedArea::ImportPopup {
self.focused = FocusedArea::Instances;
}
Ok(())
}
fn delete_config_profile(&mut self, profile: &str) -> color_eyre::Result<()> {
let instances = self.instance_manager.load_all();
for instance in instances
.into_iter()
.filter(|instance| instance.config_sync_profile.as_deref() == Some(profile))
{
let instance_dir = self.instance_manager.instances_dir.join(&instance.name);
let mut updated = instance.clone();
updated.config_sync_profile = crate::instance::config_sync::switch_profile(
&instance.name,
instance.config_sync_profile.as_deref(),
None,
&self.instance_manager.meta_dir,
&instance_dir,
)?;
self.instance_manager.save(&updated)?;
self.instances_state
.replace_instance(&instance.name, updated);
}
crate::instance::config_sync::delete_profile(&self.instance_manager.meta_dir, profile)?;
self.settings_state
.profiles
.retain(|candidate| candidate != profile);
Ok(())
}
fn remove_content_path_from_states(&mut self, path: &std::path::Path) {
self.mods_state.remove_path(path);
self.resource_packs_state.remove_path(path);
self.shaders_state.remove_path(path);
self.worlds_state.remove_path(path);
self.screenshots_state.remove_path(path);
self.logs_state.remove_path(path);
}
}
fn delete_content_path(path: &std::path::Path) -> std::io::Result<()> {
match std::fs::metadata(path) {
Ok(meta) if meta.is_dir() => std::fs::remove_dir_all(path),
Ok(_) => std::fs::remove_file(path),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}