use crate::WorkOrPersonal;
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use super::repolist::*;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
Frame, Terminal,
};
struct StatefulList {
state: ListState,
repolist: RepoList,
}
impl StatefulList {
fn with_items(repolist: RepoList) -> StatefulList {
let mut state = ListState::default();
state.select(Some(0));
StatefulList { state, repolist }
}
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.repolist.repos.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.repolist.repos.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
fn select_0(&mut self) {
self.state.select(Some(0));
}
fn go_ten_down(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.repolist.repos.len() - 10 {
self.repolist.repos.len() - 1
} else {
i + 10
}
}
None => 0,
};
self.state.select(Some(i));
}
fn go_ten_up(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i < 10 {
0
} else {
i - 10
}
}
None => 0,
};
self.state.select(Some(i));
}
fn end(&mut self) {
self.state.select(Some(self.repolist.repos.len() - 1));
}
}
struct App {
items: StatefulList,
search_text: String,
category: Option<WorkOrPersonal>,
}
impl App {
fn new(repolist: RepoList, category: Option<WorkOrPersonal>) -> App {
App {
items: StatefulList::with_items(repolist),
search_text: String::new(),
category,
}
}
}
pub fn main(
category: Option<WorkOrPersonal>,
) -> Result<std::option::Option<Launch>, Box<dyn Error>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let tick_rate = Duration::from_millis(250);
let repolist = RepoList::get_config()?;
let app = App::new(repolist, category);
let res = run_app(&mut terminal, app, tick_rate);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(res?)
}
pub enum LaunchType {
LaunchShell,
LaunchCode,
}
pub struct Launch {
pub directory: String,
pub launch_type: LaunchType,
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<Option<Launch>> {
let last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Esc => return Ok(None),
KeyCode::Left | KeyCode::Home => app.items.select_0(),
KeyCode::Right | KeyCode::End => app.items.end(),
KeyCode::Down | KeyCode::Tab => app.items.next(),
KeyCode::Up | KeyCode::BackTab => app.items.previous(),
KeyCode::PageDown => app.items.go_ten_down(),
KeyCode::PageUp => app.items.go_ten_up(),
KeyCode::Insert => {
let index = app.items.state.selected();
if let Some(index) = index {
let selected = selected(&app, index);
if let Some(selected) = selected {
return Ok(Some(Launch {
directory: selected.location.clone(),
launch_type: LaunchType::LaunchCode,
}));
}
}
return Ok(None);
}
KeyCode::Enter => {
let index = app.items.state.selected();
if let Some(index) = index {
let selected = selected(&app, index);
if let Some(selected) = selected {
return Ok(Some(Launch {
directory: selected.location.clone(),
launch_type: LaunchType::LaunchShell,
}));
}
}
return Ok(None);
}
KeyCode::Char(a) => {
app.search_text.push(a);
app.items.select_0();
}
KeyCode::Backspace => {
app.search_text.pop();
}
_ => {}
}
}
}
}
}
fn selected(app: &App, index: usize) -> Option<&Repo> {
let selected = app
.items
.repolist
.repos
.iter()
.filter(|repo| filter_category(repo, &app.category))
.filter(|repo| filter_search_text(repo, &app.search_text))
.nth(index);
selected
}
fn filter_search_text(repo: &Repo, search_text: &str) -> bool {
repo.name
.to_lowercase()
.contains(&search_text.to_lowercase())
}
fn filter_category(repo: &Repo, category: &Option<WorkOrPersonal>) -> bool {
match category {
Some(category) => category == &repo.category,
None => true,
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Percentage(85),
Constraint::Percentage(5),
]
.as_ref(),
)
.split(f.size());
let input = Paragraph::new(app.search_text.as_ref())
.block(Block::default().borders(Borders::ALL).title("Filter repos"));
f.render_widget(input, chunks[0]);
let matcher = SkimMatcherV2::default();
let mut matched = app
.items
.repolist
.repos
.iter()
.map(|repo| {
let out = matcher.fuzzy_match(&repo.name, &app.search_text);
(repo, out)
})
.filter(|x| x.1.is_some())
.map(|x| (x.0, x.1.unwrap()))
.collect::<Vec<_>>();
matched.sort_by(|a, b| a.1.cmp(&b.1));
let items: Vec<ListItem> = matched
.into_iter()
.map(|(repo, _order)| {
let mut lines = vec![Spans::from(Span::styled(
format!("{} {}", repo.name, repo.category),
Style::default().add_modifier(Modifier::BOLD),
))];
lines.push(Spans::from(Span::styled(
repo.location.clone(),
Style::default()
.fg(Color::LightCyan)
.add_modifier(Modifier::UNDERLINED)
.add_modifier(Modifier::ITALIC),
)));
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
})
.collect();
let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("Repos"))
.highlight_style(
Style::default()
.bg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(items, chunks[1], &mut app.items.state);
let bold = Style::default().add_modifier(Modifier::BOLD);
let cheatsheet = Paragraph::new(Text::from(vec![
Spans::from(vec![
Span::styled("E", bold),
Span::from("sc --> exit "),
Span::styled("E", bold),
Span::from("nter --> change directory and shell "),
Span::styled("I", bold),
Span::from("nsert --> open in vscode "),
Span::styled("A", bold),
Span::from("ny char --> to search "),
]),
Spans::from(vec![
Span::styled("L", bold),
Span::from("eft --> go to top "),
Span::styled("D", bold),
Span::from("own --> next "),
Span::styled("U", bold),
Span::from("p --> previous "),
Span::styled("B", bold),
Span::from("ackspace --> delete last character "),
]),
]));
f.render_widget(cheatsheet, chunks[2]);
}