Compare commits
4 Commits
083c66aacb
...
a135e2417f
Author | SHA1 | Date | |
---|---|---|---|
a135e2417f | |||
4691dd05d1 | |||
e7bb664cc2 | |||
823d3e8f93 |
21
src/cli.rs
21
src/cli.rs
@ -4,7 +4,7 @@ use anyhow::{ensure, Result};
|
|||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
use crate::state::write_projects_file;
|
use crate::state::write_projects_file;
|
||||||
use crate::{dirs, Projects};
|
use crate::{dirs, Project, Projects};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, clap::ValueEnum)]
|
#[derive(Debug, Clone, clap::ValueEnum)]
|
||||||
@ -76,16 +76,18 @@ where
|
|||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
let path = path::absolute(path)?;
|
let path = path::absolute(path)?;
|
||||||
|
|
||||||
ensure!(path.is_dir(), "Project path does not exists");
|
ensure!(path.is_dir(), "Project path does not exists");
|
||||||
|
|
||||||
|
let project = Project::from(path);
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
!projects.list.contains(&path.to_path_buf()),
|
!projects.list.contains(&project),
|
||||||
"Project path already registered"
|
"Project path already registered"
|
||||||
);
|
);
|
||||||
|
|
||||||
projects.list.push(path);
|
projects.list.push(project);
|
||||||
write_projects_file(projects)?;
|
write_projects_file(projects)?;
|
||||||
println!("Added {}", projects.list.last().unwrap().display());
|
println!("Added {}", projects.list.last().unwrap().path().display());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -96,16 +98,17 @@ where
|
|||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
let path = path::absolute(path)?;
|
let path = path::absolute(path)?;
|
||||||
|
let project = Project::from(path);
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
projects.list.contains(&path.to_path_buf()),
|
projects.list.contains(&project),
|
||||||
"Project path not in registry"
|
"Project path not in registry"
|
||||||
);
|
);
|
||||||
|
|
||||||
let idx =
|
let idx =
|
||||||
projects.list.iter().enumerate().find_map(
|
projects.list.iter().enumerate().find_map(
|
||||||
|(idx, elem)| {
|
|(idx, elem)| {
|
||||||
if elem == &path {
|
if elem == &project {
|
||||||
Some(idx)
|
Some(idx)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -116,7 +119,7 @@ where
|
|||||||
if let Some(idx) = idx {
|
if let Some(idx) = idx {
|
||||||
let proj = projects.list.remove(idx);
|
let proj = projects.list.remove(idx);
|
||||||
write_projects_file(projects)?;
|
write_projects_file(projects)?;
|
||||||
println!("Removed {}", proj.display());
|
println!("Removed {}", proj.path().display());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -125,7 +128,7 @@ where
|
|||||||
|
|
||||||
fn list_projects(projects: &Projects) -> Result<()> {
|
fn list_projects(projects: &Projects) -> Result<()> {
|
||||||
for (idx, project) in projects.list.iter().enumerate() {
|
for (idx, project) in projects.list.iter().enumerate() {
|
||||||
println!("{}: {}", idx + 1, project.display())
|
println!("{}: {} ({})", idx + 1, project.name(), project.path().display())
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
23
src/main.rs
23
src/main.rs
@ -21,7 +21,28 @@ struct Args {
|
|||||||
|
|
||||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||||
struct Projects {
|
struct Projects {
|
||||||
pub list: Vec<PathBuf>,
|
pub list: Vec<Project>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct Project(PathBuf);
|
||||||
|
|
||||||
|
impl Project {
|
||||||
|
pub fn path(&self) -> &PathBuf {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> String {
|
||||||
|
let name = self.0.file_name().unwrap();
|
||||||
|
name.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for Project {
|
||||||
|
fn from(value: PathBuf) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::{dirs, Projects};
|
use crate::{dirs, Project, Projects};
|
||||||
|
|
||||||
|
|
||||||
pub fn clear_selected_project_file() -> Result<()> {
|
pub fn clear_selected_project_file() -> Result<()> {
|
||||||
@ -17,8 +16,9 @@ pub fn clear_selected_project_file() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_selected_project_file(path: PathBuf) -> Result<()> {
|
pub fn write_selected_project_file(project: &Project) -> Result<()> {
|
||||||
let cache_file = dirs::selected_project_file();
|
let cache_file = dirs::selected_project_file();
|
||||||
|
let path = project.path();
|
||||||
|
|
||||||
let mut file = fs::OpenOptions::new()
|
let mut file = fs::OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
|
87
src/tui.rs
87
src/tui.rs
@ -1,17 +1,18 @@
|
|||||||
use std::path::PathBuf;
|
use std::collections::VecDeque;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
|
use ratatui::text::Span;
|
||||||
use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState};
|
use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
use tui_input::{Input, InputRequest};
|
use tui_input::{Input, InputRequest};
|
||||||
|
|
||||||
use crate::state::{clear_selected_project_file, write_selected_project_file};
|
use crate::state::{clear_selected_project_file, write_selected_project_file};
|
||||||
use crate::Projects;
|
use crate::{Project, Projects};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@ -39,8 +40,8 @@ struct State {
|
|||||||
mode: Mode,
|
mode: Mode,
|
||||||
should_exit: bool,
|
should_exit: bool,
|
||||||
project_table: TableState,
|
project_table: TableState,
|
||||||
filtered_projects: Vec<String>,
|
filtered_projects: Vec<Project>,
|
||||||
selected_project: Option<PathBuf>,
|
selected_project: Option<Project>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
@ -88,11 +89,11 @@ pub fn run(projects: Projects) -> Result<()> {
|
|||||||
if let Some(selected_project) = state.selected_project {
|
if let Some(selected_project) = state.selected_project {
|
||||||
// hacky stderr abuse
|
// hacky stderr abuse
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}cd:{}",
|
"{}-cd:{}",
|
||||||
env!("CARGO_BIN_NAME"),
|
env!("CARGO_BIN_NAME"),
|
||||||
selected_project.display()
|
selected_project.path().display()
|
||||||
);
|
);
|
||||||
write_selected_project_file(selected_project)?
|
write_selected_project_file(&selected_project)?
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -101,10 +102,10 @@ pub fn run(projects: Projects) -> Result<()> {
|
|||||||
|
|
||||||
fn handle_key_event(state: &mut State, tx: &mut mpsc::Sender<Message>, event: KeyEvent) {
|
fn handle_key_event(state: &mut State, tx: &mut mpsc::Sender<Message>, event: KeyEvent) {
|
||||||
let msg = match (&state.mode, event.modifiers, event.code) {
|
let msg = match (&state.mode, event.modifiers, event.code) {
|
||||||
(Mode::Search, KeyModifiers::CONTROL, KeyCode::Char('k') | KeyCode::Up) => {
|
(Mode::Search, KeyModifiers::CONTROL, KeyCode::Char('p' | 'k') | KeyCode::Up) => {
|
||||||
Message::SelectPrevious
|
Message::SelectPrevious
|
||||||
}
|
}
|
||||||
(Mode::Search, KeyModifiers::CONTROL, KeyCode::Char('j') | KeyCode::Down) => {
|
(Mode::Search, KeyModifiers::CONTROL, KeyCode::Char('n' | 'j') | KeyCode::Down) => {
|
||||||
Message::SelectNext
|
Message::SelectNext
|
||||||
}
|
}
|
||||||
(Mode::Search, KeyModifiers::NONE, KeyCode::Esc) => Message::Exit,
|
(Mode::Search, KeyModifiers::NONE, KeyCode::Esc) => Message::Exit,
|
||||||
@ -152,7 +153,7 @@ fn handle_messages(state: &mut State, rx: &mut mpsc::Receiver<Message>) -> Resul
|
|||||||
if let Some(selected) = state.project_table.selected() {
|
if let Some(selected) = state.project_table.selected() {
|
||||||
if let Some(project_path) = state.filtered_projects.get(selected) {
|
if let Some(project_path) = state.filtered_projects.get(selected) {
|
||||||
state.should_exit = true;
|
state.should_exit = true;
|
||||||
state.selected_project = Some(PathBuf::from(project_path));
|
state.selected_project = Some(project_path.clone());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,31 +209,43 @@ fn draw_search(state: &mut State, frame: &mut Frame, area: Rect) {
|
|||||||
|
|
||||||
|
|
||||||
fn draw_list(state: &mut State, frame: &mut Frame, area: Rect) {
|
fn draw_list(state: &mut State, frame: &mut Frame, area: Rect) {
|
||||||
state.filtered_projects = state
|
let search_value = state.search.value();
|
||||||
.projects
|
let projects = &state.projects.list;
|
||||||
.list
|
|
||||||
.iter()
|
|
||||||
.filter_map(|project| {
|
|
||||||
let path_str = project.to_str().expect("invalid path string");
|
|
||||||
|
|
||||||
let search_value = state.search.value();
|
state.filtered_projects = if search_value.is_empty() {
|
||||||
|
projects.clone()
|
||||||
|
} else {
|
||||||
|
let mut filtered = VecDeque::new();
|
||||||
|
|
||||||
if search_value.is_empty() {
|
for project in projects.iter() {
|
||||||
return Some(path_str.to_string());
|
match fuzzy_search(search_value, &project.name()) {
|
||||||
|
Some(indices) => {
|
||||||
|
trace!(?search_value, ?project, ?indices);
|
||||||
|
filtered.push_front(project.clone());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let path = project.path().to_str().unwrap();
|
||||||
|
if let Some(indices) = fuzzy_search(search_value, path) {
|
||||||
|
trace!(?search_value, ?project, ?indices);
|
||||||
|
filtered.push_back(project.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let indices = fuzzy_search(search_value, path_str)?;
|
Vec::from(filtered)
|
||||||
trace!(?search_value, ?path_str, ?indices);
|
};
|
||||||
Some(path_str.to_string())
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rows = state
|
let rows = state.filtered_projects.iter().map(|project| {
|
||||||
.filtered_projects
|
let name = project.name();
|
||||||
.iter()
|
let path_style = Style::new().fg(Color::DarkGray);
|
||||||
.map(|path| Row::new([Cell::new(path.as_str())]));
|
let path_span = Span::styled(project.path().display().to_string(), path_style);
|
||||||
|
let name_span = Span::from(name);
|
||||||
|
|
||||||
let widths = [Constraint::Min(20)];
|
Row::new([Cell::new(name_span), Cell::new(path_span)])
|
||||||
|
});
|
||||||
|
|
||||||
|
let widths = [Constraint::Min(20), Constraint::Fill(1)];
|
||||||
|
|
||||||
let table_highlight = Style::default().fg(Color::Blue);
|
let table_highlight = Style::default().fg(Color::Blue);
|
||||||
|
|
||||||
@ -246,13 +259,25 @@ fn fuzzy_search(search: &str, text: &str) -> Option<Vec<usize>> {
|
|||||||
let mut found_indices = Vec::new();
|
let mut found_indices = Vec::new();
|
||||||
let mut start_idx = 0;
|
let mut start_idx = 0;
|
||||||
|
|
||||||
for ch in search.chars() {
|
let mut case_sensitive = false;
|
||||||
|
|
||||||
|
for (ch_idx, ch) in search.char_indices() {
|
||||||
|
if ch_idx == 0 {
|
||||||
|
case_sensitive = ch.is_uppercase();
|
||||||
|
}
|
||||||
|
|
||||||
let remaining = &text[start_idx..];
|
let remaining = &text[start_idx..];
|
||||||
|
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
|
|
||||||
for (byte_idx, txt_ch) in remaining.char_indices() {
|
for (byte_idx, txt_ch) in remaining.char_indices() {
|
||||||
if ch == txt_ch {
|
let matching = if case_sensitive {
|
||||||
|
ch == txt_ch
|
||||||
|
} else {
|
||||||
|
ch.to_lowercase().cmp(txt_ch.to_lowercase()).is_eq()
|
||||||
|
};
|
||||||
|
|
||||||
|
if matching {
|
||||||
found_indices.push(start_idx + byte_idx);
|
found_indices.push(start_idx + byte_idx);
|
||||||
start_idx += byte_idx + txt_ch.len_utf8();
|
start_idx += byte_idx + txt_ch.len_utf8();
|
||||||
found = true;
|
found = true;
|
||||||
|
Loading…
Reference in New Issue
Block a user