pub mod filter;
pub mod graph;
mod input;
mod render;
use filter::FilterMode;
use crate::engine::{format_size, same_session};
use crate::model::World;
use crossterm::event::{
self, Event, KeyCode, KeyEventKind, KeyModifiers, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
use nucleo_matcher::{Matcher, Utf32Str};
use ratatui::prelude::*;
use std::io::{self, Stdout};
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Relation {
NeededBy,
DependsOn,
}
impl Relation {
pub(crate) fn toggle(self) -> Self {
match self {
Relation::NeededBy => Relation::DependsOn,
Relation::DependsOn => Relation::NeededBy,
}
}
}
pub(crate) struct Frame {
pub(crate) focus: Option<String>,
pub(crate) pool: Vec<String>,
pub(crate) needed_by: Vec<String>,
pub(crate) depends_on: Vec<String>,
pub(crate) query: String,
pub(crate) selected: usize,
pub(crate) alongside: Vec<String>,
pub(crate) origin: Origin,
}
pub(crate) enum Origin {
None,
Manual,
PulledIn(String),
Untraced,
}
pub(crate) struct App {
pub(crate) world: World,
pub(crate) matcher: Matcher,
pub(crate) filter: FilterMode,
pub(crate) relation: Relation,
pub(crate) stack: Vec<Frame>,
pub(crate) graph: Option<graph::GraphView>,
}
impl App {
pub(crate) fn run_ui(&mut self) -> io::Result<()> {
let mut terminal = setup_terminal()?;
let result = self.event_loop(&mut terminal);
restore_terminal(&mut terminal)?;
result
}
pub(crate) fn has_flatpak(&self) -> bool {
self.world
.packages
.values()
.any(|p| p.source == crate::model::Source::Flatpak)
}
pub(crate) fn frame(&self) -> &Frame {
self.stack.last().unwrap()
}
pub(crate) fn frame_mut(&mut self) -> &mut Frame {
self.stack.last_mut().unwrap()
}
pub(crate) fn open(&mut self, pkg: String) {
let dedup = |src: &[String]| -> Vec<String> {
let mut seen = std::collections::HashSet::new();
src.iter()
.filter(|p| seen.insert((*p).clone()))
.cloned()
.collect()
};
let needed_by = dedup(self.world.rdeps_of(&pkg));
let depends_on = dedup(self.world.deps_of(&pkg));
let origin = if self.world.is_manual(&pkg) {
Origin::Manual
} else {
match crate::engine::bfs_root(&self.world, &pkg) {
Some(path) => Origin::PulledIn(path.last().cloned().unwrap_or_default()),
None => Origin::Untraced,
}
};
let alongside = same_session(&self.world, &pkg);
self.relation = Relation::NeededBy;
self.stack.push(Frame {
focus: Some(pkg),
pool: Vec::new(),
needed_by,
depends_on,
query: String::new(),
selected: 0,
alongside,
origin,
});
}
pub(crate) fn base_list(&self) -> &[String] {
let frame = self.stack.last().unwrap();
if frame.focus.is_none() {
&frame.pool
} else if self.relation == Relation::NeededBy {
&frame.needed_by
} else {
&frame.depends_on
}
}
pub(crate) fn clamp_selection(&mut self, len: usize) {
let sel = &mut self.frame_mut().selected;
if len == 0 {
*sel = 0;
} else if *sel >= len {
*sel = len - 1;
}
}
pub(crate) fn move_selection(&mut self, delta: isize, len: usize) {
if len == 0 {
return;
}
let cur = self.frame().selected as isize;
let next = (cur + delta).clamp(0, len as isize - 1);
self.frame_mut().selected = next as usize;
}
}
fn setup_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
if matches!(
crossterm::terminal::supports_keyboard_enhancement(),
Ok(true)
) {
let _ = execute!(
stdout,
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
);
}
Terminal::new(CrosstermBackend::new(stdout))
}
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> io::Result<()> {
if matches!(
crossterm::terminal::supports_keyboard_enhancement(),
Ok(true)
) {
let _ = execute!(terminal.backend_mut(), PopKeyboardEnhancementFlags);
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()
}