Skip to content

Commit

Permalink
feat: add unique vault names (#18)
Browse files Browse the repository at this point in the history
When multiple vaults have identical names, rofi-obsidian will find the
shortest unique path to display.

Requires the "-n unique" flag to be set or display_name = "unique" to be
present in the configuration file.

Closes #4
  • Loading branch information
nydragon authored May 29, 2024
2 parents 8005230 + 10c9371 commit c7bc059
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 9 deletions.
4 changes: 4 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::fmt::Display;

use clap::{Parser, ValueEnum};

use crate::config::DisplayName;

#[derive(Debug, Clone, Default, ValueEnum)]
pub enum SubCommand {
/// Initiate the configuration at the default location
Expand Down Expand Up @@ -32,4 +34,6 @@ pub struct Args {
pub command: SubCommand,
#[clap()]
pub selection: Option<String>,
#[clap(short, long, help = "The style of the vault name")]
pub name: Option<DisplayName>,
}
4 changes: 3 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use anyhow::Result;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::{
fs::{self, create_dir_all, write},
path::PathBuf,
};

#[derive(Serialize, Deserialize, Default, Debug)]
#[derive(Serialize, Deserialize, ValueEnum, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DisplayName {
#[default]
VaultName,
Path,
Unique,
}

#[derive(Serialize, Deserialize, Debug, Default)]
Expand Down
174 changes: 174 additions & 0 deletions src/display_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use std::{fmt::Debug, path::PathBuf};

/// Shorten two iterables to the first element they do not have in common (inclusive).
fn get_divergence<E>(e1: E, e2: E) -> (E, E)
where
<E as IntoIterator>::Item: Eq + PartialEq + Debug + Default,
E: IntoIterator + Default + Extend<<E as IntoIterator>::Item>,
{
let mut diverged = false;

e1.into_iter()
.zip(e2)
.take_while(|(e1_e, e2_e)| {
if diverged {
!diverged
} else {
diverged = e1_e != e2_e;
true
}
})
.unzip()
}

/// Split the path string accurately, that is, respecting escaping backslashes
fn split_path(s: &String) -> Vec<String> {
// TODO: Find a better way to do this
PathBuf::from(s)
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned())
.rev()
.collect()
}

/// Shorten all vault paths to the shortest unique value
/// Performance is probably not great, but we shouldn't have too many values to process
pub fn make_unique(vaults: Vec<String>) -> Vec<String> {
let v: Vec<Vec<String>> = vaults.iter().map(split_path).collect();

v.iter()
.enumerate()
.map(|(i, va)| {
let mut v = v.clone();
v.remove(i);

let mut res: Vec<String> = v
.iter()
.map(|e| get_divergence(va.to_vec(), e.to_vec()).0)
.max()
.unwrap_or(va.to_vec());

res.reverse();
res.join("/")
})
.collect()
}

#[cfg(test)]
mod tests {

mod differentiate {
use crate::display_name::make_unique;

#[test]
fn different_name_same_path() {
let vaults = vec![
String::from("~/Documents/personal"),
String::from("~/Documents/work"),
];

let names = make_unique(vaults);

assert_eq!(names[0], "personal");
assert_eq!(names[1], "work");
}

#[test]
fn same_name_different_parent() {
let vaults = vec![
String::from("~/Downloads/personal"),
String::from("~/Documents/personal"),
];

let names = make_unique(vaults);

assert_eq!(names[0], "Downloads/personal");
assert_eq!(names[1], "Documents/personal");
}

#[test]
fn same_name_same_parent() {
let vaults = vec![
String::from("~/Downloads/vaults/personal"),
String::from("~/Documents/vaults/personal"),
];

let names = make_unique(vaults);

assert_eq!(names[0], "Downloads/vaults/personal");
assert_eq!(names[1], "Documents/vaults/personal");
}

#[test]
fn many() {
let vaults = vec![
String::from("~/Downloads/vaults/personal"),
String::from("~/Documents/vaults/personal"),
String::from("~/Downloads/personal"),
String::from("~/Documents/personal"),
String::from("~/Documents/work"),
];

let names = make_unique(vaults);

assert_eq!(names[0], "Downloads/vaults/personal");
assert_eq!(names[1], "Documents/vaults/personal");
assert_eq!(names[2], "Downloads/personal");
assert_eq!(names[3], "Documents/personal");
assert_eq!(names[4], "work");
}
}

mod get_divergence {
use crate::display_name::get_divergence;

#[test]
fn test_get_divergence() {
let v1 = vec![1, 2, 3, 4];
let v2 = vec![1, 2, 4, 4];

let (v1_d, v2_d) = get_divergence(v1, v2);

assert_eq!(v1_d[..v1_d.len() - 1], v2_d[..v2_d.len() - 1]);
assert_eq!(v1_d.len(), 3);
assert_eq!(v2_d.len(), 3);
assert_ne!(v1_d.last(), v2_d.last());
}

#[test]
fn test_get_divergence_identical() {
let v1 = vec![1, 2, 3, 4];
let v2 = vec![1, 2, 3, 4];

let (v1_d, v2_d) = get_divergence(v1, v2);

assert_eq!(v1_d, v2_d);
}

#[test]
fn test_first_elem_diverge() {
let v1 = vec![2, 2, 3, 4];
let v2 = vec![1, 2, 3, 4];

let (v1_d, v2_d) = get_divergence(v1, v2);

assert_eq!(v1_d.len(), 1);
assert_eq!(v2_d.len(), 1);
assert_ne!(v1_d[0], v2_d[0]);
assert_ne!(v1_d.last(), v2_d.last());
}

#[test]
fn test_unequal_length() {
let v1 = vec![1, 3];
let v2 = vec![1, 2, 3, 4];

let (v1_d, v2_d) = get_divergence(v1, v2);

assert_eq!(v1_d.len(), 2);
assert_eq!(v2_d.len(), 2);
assert_eq!(v1_d[0], v2_d[0]);
assert_ne!(v1_d.last(), v2_d.last());
}
}
}
28 changes: 20 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ use anyhow::Result;
use args::{Args, SubCommand};
use clap::Parser;
use config::Config;
use display_name::make_unique;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::path::Path;
use std::{env, fs};
use url::form_urlencoded::Serializer;

mod args;
mod config;
mod display_name;

#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct VaultDB {
Expand Down Expand Up @@ -66,9 +69,7 @@ fn build_sources(conf: &Config) -> Vec<String> {
}

fn get_known_vaults(conf: &Config) -> Vec<String> {
let sources = build_sources(conf);

let mut vaults = sources
let mut vaults = build_sources(conf)
.iter()
.flat_map(|path| get_vaults(path.to_string()).unwrap_or_default())
.collect::<HashSet<String>>()
Expand All @@ -80,19 +81,30 @@ fn get_known_vaults(conf: &Config) -> Vec<String> {
vaults
}

fn rofi_main(state: u8, conf: Config, _args: Args) -> Result<()> {
fn rofi_main(state: u8, conf: Config, args: Args) -> Result<()> {
let rofi_info: String = env::var("ROFI_INFO").unwrap_or_default();
let name_style = args.name.unwrap_or(conf.display_name.clone());

match state {
// Prompting which vault to open
0 => {
get_known_vaults(&conf).iter().for_each(|vault| {
let name = match conf.display_name {
let vaults = get_known_vaults(&conf);
// TODO: Lazy evaluation would be cooler: https://github.com/rust-lang/rust/issues/109736
let unique_names: Vec<String> = if name_style == DisplayName::Unique {
make_unique(vaults.clone())
} else {
vec![]
};

vaults.iter().enumerate().for_each(|(i, vault)| {
let name = match name_style {
DisplayName::VaultName => Path::new(vault)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_else(|| vault),
DisplayName::Path => vault,
//DisplayName::Unique => unique_names.get(i).unwrap_or(vault),
DisplayName::Unique => unique_names.get(i).unwrap(),
};

println!("{name}\0info\x1f{vault}");
Expand Down Expand Up @@ -152,14 +164,14 @@ mod tests {

#[test]
fn test_base_json() {
let paths = get_vaults("./test_assets/base.json".into()).unwrap();
let paths = get_vaults("./test_assets/base.json".to_string()).unwrap();

assert_eq!(paths.len(), 2);
}

#[test]
fn test_extra_fields_json() {
let paths = get_vaults("./test_assets/extra_fields.json".into()).unwrap();
let paths = get_vaults("./test_assets/extra_fields.json".to_string()).unwrap();

assert_eq!(paths.len(), 2);
}
Expand Down

0 comments on commit c7bc059

Please sign in to comment.