semora_cli/commands/
map.rs1use anyhow::bail;
4use semora_proto::{Mapping, MappingList};
5
6use crate::args::{MapArgs, MapCmd};
7use crate::client::Client;
8use crate::{Ctx, exit, output, project_link};
9
10pub async fn run(ctx: &Ctx, args: MapArgs) -> anyhow::Result<i32> {
11 let client = ctx.client()?;
12 let slug = project_slug(&client, &ctx.settings.config_dir, args.project.as_deref()).await?;
13 match args.cmd {
14 MapCmd::Ls => {
15 let list = client.mappings(&slug).await?.mappings;
16 show(ctx, &slug, &list);
17 }
18 MapCmd::Add {
19 pattern,
20 folder,
21 at,
22 } => {
23 reject_drive_path("PATTERN", &pattern)?;
25 reject_drive_path("FOLDER", &folder)?;
26 let mut list = client.mappings(&slug).await?.mappings;
27 let at = match at {
28 None => list.len() + 1,
29 Some(n) if (1..=list.len() + 1).contains(&n) => n,
30 Some(_) => bail!("{}; --at can be 1 to {}", count(list.len()), list.len() + 1),
31 };
32 list.insert(at - 1, Mapping { pattern, folder });
33 let stored = client.set_mappings(&slug, &list).await?.mappings;
34 if !ctx.json {
35 let done = output::out().done;
36 anstream::println!(
37 "{done}Added{done:#} rule {at}: {} → {}",
38 stored[at - 1].pattern,
39 stored[at - 1].folder
40 );
41 }
42 show(ctx, &slug, &stored);
43 }
44 MapCmd::Rm { n } => {
45 let mut list = client.mappings(&slug).await?.mappings;
46 if n == 0 || n > list.len() {
47 bail!(
48 "there is no rule {n}; there {} {}",
49 if list.len() == 1 { "is" } else { "are" },
50 list.len()
51 );
52 }
53 let gone = list.remove(n - 1);
54 let stored = client.set_mappings(&slug, &list).await?.mappings;
55 if !ctx.json {
56 let done = output::out().done;
57 anstream::println!(
58 "{done}Removed{done:#} rule {n}: {} → {}",
59 gone.pattern,
60 gone.folder
61 );
62 }
63 show(ctx, &slug, &stored);
64 }
65 }
66 Ok(exit::OK)
67}
68
69fn count(n: usize) -> String {
71 if n == 1 {
72 "there is 1 rule".to_string()
73 } else {
74 format!("there are {n} rules")
75 }
76}
77
78fn reject_drive_path(what: &str, value: &str) -> anyhow::Result<()> {
81 let b = value.as_bytes();
82 if b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && b[2] == b'/' {
83 bail!(
84 "{what} looks like a drive path. Your shell may have rewritten a leading \"/\" (Git Bash on Windows does). Leave the slash off, or use \".\" for the project root."
85 );
86 }
87 Ok(())
88}
89
90pub async fn project_slug(
93 client: &Client,
94 config_dir: &std::path::Path,
95 flag: Option<&str>,
96) -> anyhow::Result<String> {
97 if let Some(slug) = flag {
98 return Ok(slug.to_string());
99 }
100 let here = std::env::current_dir()?;
101 match project_link::find(&here)? {
104 Some(file) => {
105 let link = project_link::resolve(client, config_dir, file).await?;
106 Ok(project_link::follow(client, link).await?.slug)
107 }
108 None => {
109 bail!("no project: pass --project ID or run this inside a folder with a .semora file")
110 }
111 }
112}
113
114fn show(ctx: &Ctx, slug: &str, list: &[Mapping]) {
115 if ctx.json {
116 output::json(&MappingList {
117 mappings: list.to_vec(),
118 });
119 } else {
120 print_list(slug, list);
121 }
122}
123
124pub fn print_list(slug: &str, list: &[Mapping]) {
125 if list.is_empty() {
126 println!("No mappings for {slug}. Every document shows at its real path.");
127 return;
128 }
129 println!(
130 "Mappings for {slug} (the first match wins; a path no rule matches stays where it is)"
131 );
132 let number_width = list.len().to_string().len();
133 let pattern_width = list
134 .iter()
135 .map(|m| m.pattern.chars().count())
136 .max()
137 .unwrap_or(0);
138 for (i, m) in list.iter().enumerate() {
139 println!(
140 " {:>number_width$} {:<pattern_width$} → {}",
141 i + 1,
142 m.pattern,
143 m.folder
144 );
145 }
146}