use super::super::{
App, SidebarItemKind,
state::{GoToDestination, GoToOverlay, GoToOverlayRow},
};
use crate::fs::{rect_contains, trash_dir};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use std::path::PathBuf;
impl App {
pub fn goto_is_open(&self) -> bool {
self.overlays.goto.is_some()
}
pub fn goto_title(&self) -> &str {
self.overlays
.goto
.as_ref()
.map(|overlay| overlay.title.as_str())
.unwrap_or("")
}
pub fn goto_row_count(&self) -> usize {
self.overlays
.goto
.as_ref()
.map(|overlay| overlay.rows.len())
.unwrap_or(0)
}
pub fn goto_row_label(&self, index: usize) -> &str {
self.overlays
.goto
.as_ref()
.and_then(|overlay| overlay.rows.get(index))
.map(|row| row.label.as_str())
.unwrap_or("")
}
pub fn goto_row_shortcut(&self, index: usize) -> Option<char> {
self.overlays
.goto
.as_ref()
.and_then(|overlay| overlay.rows.get(index))
.map(|row| row.shortcut)
}
}
impl App {
pub(in crate::app) fn open_goto_overlay(&mut self) {
self.overlays.help = false;
self.overlays.goto = Some(build_goto_overlay(self));
self.status.clear();
}
pub(in crate::app) fn handle_goto_key(&mut self, key: KeyEvent) -> Result<()> {
if key.modifiers.contains(KeyModifiers::CONTROL) && matches!(key.code, KeyCode::Char('c')) {
self.overlays.goto = None;
return Ok(());
}
match key.code {
KeyCode::Esc => {
self.overlays.goto = None;
}
KeyCode::Char(ch)
if !key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
{
if let Some(index) = self.goto_row_index_for_shortcut(ch) {
self.confirm_goto_index(index)?;
}
}
_ => {}
}
Ok(())
}
pub(in crate::app) fn handle_goto_mouse(&mut self, mouse: MouseEvent) -> Result<()> {
if let MouseEventKind::Down(MouseButton::Left) = mouse.kind {
let inside = self
.input
.frame_state
.goto_panel
.is_some_and(|panel| rect_contains(panel, mouse.column, mouse.row));
if !inside {
self.overlays.goto = None;
return Ok(());
}
if let Some(hit) = self
.input
.frame_state
.goto_hits
.iter()
.find(|hit| rect_contains(hit.rect, mouse.column, mouse.row))
.cloned()
{
self.confirm_goto_index(hit.index)?;
}
}
Ok(())
}
fn goto_row_index_for_shortcut(&self, ch: char) -> Option<usize> {
let needle = ch.to_ascii_lowercase();
self.overlays.goto.as_ref().and_then(|overlay| {
overlay
.rows
.iter()
.position(|row| row.shortcut.to_ascii_lowercase() == needle)
})
}
fn confirm_goto_index(&mut self, index: usize) -> Result<()> {
let Some(destination) = self
.overlays
.goto
.as_ref()
.and_then(|overlay| overlay.rows.get(index).map(|row| row.destination.clone()))
else {
return Ok(());
};
match destination {
GoToDestination::Top => {
self.overlays.goto = None;
self.select_index(0);
}
GoToDestination::Path(path) => {
self.overlays.goto = None;
self.set_dir(path)?;
}
GoToDestination::Missing(status) => {
self.status = status;
}
}
Ok(())
}
}
fn build_goto_overlay(app: &App) -> GoToOverlay {
let rows = vec![
build_goto_row('g', "top", GoToDestination::Top),
build_goto_row(
'd',
"downloads",
downloads_destination(app)
.map(GoToDestination::Path)
.unwrap_or_else(|| GoToDestination::Missing("Downloads not available".to_string())),
),
build_goto_row(
'h',
"home",
crate::fs::home_dir()
.map(GoToDestination::Path)
.unwrap_or_else(|| GoToDestination::Missing("Home not available".to_string())),
),
build_goto_row(
'c',
config_label(),
config_directory()
.map(GoToDestination::Path)
.unwrap_or_else(|| {
GoToDestination::Missing(format!("{} not available", config_label()))
}),
),
build_goto_row(
't',
"trash",
trash_destination(app)
.map(GoToDestination::Path)
.unwrap_or_else(|| GoToDestination::Missing("Trash not available".to_string())),
),
];
GoToOverlay {
title: "Go to".to_string(),
rows,
}
}
fn build_goto_row(shortcut: char, label: &str, destination: GoToDestination) -> GoToOverlayRow {
GoToOverlayRow {
shortcut,
label: label.to_string(),
destination,
}
}
fn config_label() -> &'static str {
if cfg!(target_os = "macos") {
"App Support"
} else if cfg!(windows) {
"AppData"
} else {
".config"
}
}
fn downloads_destination(app: &App) -> Option<PathBuf> {
app.navigation
.sidebar
.iter()
.filter_map(|row| row.item())
.find(|item| item.kind == SidebarItemKind::Downloads)
.map(|item| item.path.clone())
.or_else(|| crate::fs::home_dir().map(|home| home.join("Downloads")))
.filter(|path| path.exists())
}
fn config_directory() -> Option<PathBuf> {
let dir = crate::config::config_dir()?;
dir.parent().map(PathBuf::from)
}
fn trash_destination(app: &App) -> Option<PathBuf> {
app.navigation
.sidebar
.iter()
.filter_map(|row| row.item())
.find(|item| item.kind == SidebarItemKind::Trash)
.map(|item| item.path.clone())
.or_else(|| crate::fs::home_dir().and_then(|home| trash_dir(&home)))
}