Compare commits
2 Commits
7f5c716c7a
...
3895791138
Author | SHA1 | Date | |
---|---|---|---|
|
3895791138 | ||
3fdd9f93a9
|
@@ -6,9 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [0.0.6] - 2025-07-26
|
## [0.1.0] - 2025-08-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- add file opener with chooser
|
||||||
- update demo
|
- update demo
|
||||||
- add help text for preview
|
- add help text for preview
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.6"
|
version = "0.1.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
noil = { path = "crates/noil" }
|
noil = { path = "crates/noil" }
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::edit::apply,
|
cli::edit::{ApplyOptions, apply},
|
||||||
commit::{Action, print_changes},
|
commit::{Action, print_changes},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -9,6 +11,9 @@ use crate::{
|
|||||||
pub struct ApplyCommand {
|
pub struct ApplyCommand {
|
||||||
#[arg(long = "commit")]
|
#[arg(long = "commit")]
|
||||||
commit: bool,
|
commit: bool,
|
||||||
|
|
||||||
|
#[arg(long = "chooser-file", env = "NOIL_CHOOSER_FILE")]
|
||||||
|
chooser_file: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplyCommand {
|
impl ApplyCommand {
|
||||||
@@ -24,7 +29,15 @@ impl ApplyCommand {
|
|||||||
let action = print_changes(&input, !self.commit).await?;
|
let action = print_changes(&input, !self.commit).await?;
|
||||||
let res = match action {
|
let res = match action {
|
||||||
Action::Quit => Ok(()),
|
Action::Quit => Ok(()),
|
||||||
Action::Apply { original } => apply(&original).await,
|
Action::Apply { original } => {
|
||||||
|
apply(
|
||||||
|
&original,
|
||||||
|
ApplyOptions {
|
||||||
|
chooser_file: self.chooser_file.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
Action::Edit => todo!(),
|
Action::Edit => todo!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +45,13 @@ impl ApplyCommand {
|
|||||||
|
|
||||||
res
|
res
|
||||||
} else {
|
} else {
|
||||||
apply(&input).await
|
apply(
|
||||||
|
&input,
|
||||||
|
ApplyOptions {
|
||||||
|
chooser_file: self.chooser_file.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,6 +23,9 @@ const PREVIEW: bool = false;
|
|||||||
pub struct EditCommand {
|
pub struct EditCommand {
|
||||||
#[arg()]
|
#[arg()]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long = "chooser-file", env = "NOIL_CHOOSER_FILE")]
|
||||||
|
chooser_file: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditCommand {
|
impl EditCommand {
|
||||||
@@ -89,7 +92,13 @@ impl EditCommand {
|
|||||||
match action {
|
match action {
|
||||||
Action::Quit => return Ok(()),
|
Action::Quit => return Ok(()),
|
||||||
Action::Apply { original } => {
|
Action::Apply { original } => {
|
||||||
return apply(&original).await;
|
return apply(
|
||||||
|
&original,
|
||||||
|
ApplyOptions {
|
||||||
|
chooser_file: self.chooser_file.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
Action::Edit => continue,
|
Action::Edit => continue,
|
||||||
}
|
}
|
||||||
@@ -107,6 +116,11 @@ async fn wait_user() -> Result<(), anyhow::Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub struct ApplyOptions {
|
||||||
|
pub chooser_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
/// the philosphy behind apply is that we try unlike normal file system operations to be idempotent.
|
/// the philosphy behind apply is that we try unlike normal file system operations to be idempotent.
|
||||||
/// This is mainly for 2 reasons.
|
/// This is mainly for 2 reasons.
|
||||||
///
|
///
|
||||||
@@ -114,17 +128,27 @@ async fn wait_user() -> Result<(), anyhow::Error> {
|
|||||||
/// 2. A .noil recipe can be rerun, having small issues disrupt the work would be counterproductive, as the .noil language is not powerful enough to handle the flexibility required for file checking
|
/// 2. A .noil recipe can be rerun, having small issues disrupt the work would be counterproductive, as the .noil language is not powerful enough to handle the flexibility required for file checking
|
||||||
///
|
///
|
||||||
/// All in all apply is mostly idempotent, and won't override files, it tries to be as non destructive as possible. For example move will only throw a warning if the source file doesn't exists, but the destination does
|
/// All in all apply is mostly idempotent, and won't override files, it tries to be as non destructive as possible. For example move will only throw a warning if the source file doesn't exists, but the destination does
|
||||||
pub async fn apply(input: &str) -> anyhow::Result<()> {
|
pub async fn apply(input: &str, options: ApplyOptions) -> anyhow::Result<()> {
|
||||||
eprintln!("applying changes");
|
eprintln!("applying changes");
|
||||||
|
|
||||||
let noil_index = parse::parse_input(input).context("parse input")?;
|
let noil_index = parse::parse_input(input).context("parse input")?;
|
||||||
|
|
||||||
|
let mut open_files = Vec::new();
|
||||||
|
|
||||||
for file in &noil_index.files {
|
for file in &noil_index.files {
|
||||||
let path = &file.path;
|
let path = &file.path;
|
||||||
match &file.entry.operation {
|
match &file.entry.operation {
|
||||||
Operation::Existing { .. } => {
|
Operation::Existing { .. } => {
|
||||||
// Noop
|
// Noop
|
||||||
}
|
}
|
||||||
|
Operation::Open { .. } => {
|
||||||
|
if path.to_string_lossy().ends_with("/") {
|
||||||
|
// We can't open directories, so they're skipped
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
open_files.push(path);
|
||||||
|
}
|
||||||
Operation::Add => {
|
Operation::Add => {
|
||||||
tracing::debug!("creating file");
|
tracing::debug!("creating file");
|
||||||
|
|
||||||
@@ -230,6 +254,31 @@ pub async fn apply(input: &str) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(chooser_file) = &options.chooser_file {
|
||||||
|
tracing::debug!("creating chooser file");
|
||||||
|
if let Some(parent) = chooser_file.parent()
|
||||||
|
&& !chooser_file.exists()
|
||||||
|
{
|
||||||
|
tokio::fs::create_dir_all(parent)
|
||||||
|
.await
|
||||||
|
.context("parent dir for chooser file")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = tokio::fs::File::create(chooser_file)
|
||||||
|
.await
|
||||||
|
.context("create new chooser file")?;
|
||||||
|
|
||||||
|
let open_files = open_files
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.display().to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
file.write_all(open_files.join("\n").as_bytes())
|
||||||
|
.await
|
||||||
|
.context("write chosen files")?;
|
||||||
|
file.flush().await.context("flush chosen file")?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,6 +23,9 @@ pub(crate) fn format(input: &str) -> anyhow::Result<String> {
|
|||||||
| models::Operation::Delete { index }
|
| models::Operation::Delete { index }
|
||||||
| models::Operation::Move { index }
|
| models::Operation::Move { index }
|
||||||
| models::Operation::Existing { index } => index.len(),
|
| models::Operation::Existing { index } => index.len(),
|
||||||
|
models::Operation::Open { index } => {
|
||||||
|
index.as_ref().map(|i| i.len()).unwrap_or_default()
|
||||||
|
}
|
||||||
models::Operation::Add => 0,
|
models::Operation::Add => 0,
|
||||||
})
|
})
|
||||||
.max()
|
.max()
|
||||||
@@ -53,6 +56,7 @@ pub(crate) fn format(input: &str) -> anyhow::Result<String> {
|
|||||||
| models::Operation::Delete { index }
|
| models::Operation::Delete { index }
|
||||||
| models::Operation::Move { index }
|
| models::Operation::Move { index }
|
||||||
| models::Operation::Existing { index } => Some(index),
|
| models::Operation::Existing { index } => Some(index),
|
||||||
|
models::Operation::Open { index } => index,
|
||||||
models::Operation::Add => None,
|
models::Operation::Add => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -35,6 +35,7 @@ pub enum Operation {
|
|||||||
Copy { index: String },
|
Copy { index: String },
|
||||||
Delete { index: String },
|
Delete { index: String },
|
||||||
Move { index: String },
|
Move { index: String },
|
||||||
|
Open { index: Option<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Operation {
|
impl Display for Operation {
|
||||||
@@ -45,6 +46,7 @@ impl Display for Operation {
|
|||||||
Operation::Copy { .. } => "COPY",
|
Operation::Copy { .. } => "COPY",
|
||||||
Operation::Delete { .. } => "DELETE",
|
Operation::Delete { .. } => "DELETE",
|
||||||
Operation::Move { .. } => "MOVE",
|
Operation::Move { .. } => "MOVE",
|
||||||
|
Operation::Open { .. } => "OPEN",
|
||||||
};
|
};
|
||||||
|
|
||||||
f.write_str(op)
|
f.write_str(op)
|
||||||
@@ -86,6 +88,16 @@ impl FileEntry {
|
|||||||
"D" | "DEL" | "DELETE" if first != last => Operation::Delete { index },
|
"D" | "DEL" | "DELETE" if first != last => Operation::Delete { index },
|
||||||
// MOVE:
|
// MOVE:
|
||||||
"M" | "MV" | "MOVE" | "RENAME" if first != last => Operation::Move { index },
|
"M" | "MV" | "MOVE" | "RENAME" if first != last => Operation::Move { index },
|
||||||
|
"O" | "OPEN" => Operation::Open {
|
||||||
|
index: {
|
||||||
|
// if LAST == is the Operation, we set the index to empty, if the index is missing we set it to None
|
||||||
|
if index.is_empty() || index.chars().any(|c| c.is_uppercase()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
o => {
|
o => {
|
||||||
anyhow::bail!("operation: {} is not supported", o);
|
anyhow::bail!("operation: {} is not supported", o);
|
||||||
}
|
}
|
||||||
@@ -194,6 +206,60 @@ ADD : /var/my/path/new-long-path
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_parse_item_open_operation() -> anyhow::Result<()> {
|
||||||
|
let input = r#"
|
||||||
|
O abc : /var/my
|
||||||
|
OPEN ecd : /var/my/path
|
||||||
|
O : /var/my/path/new-path
|
||||||
|
OPEN : /var/my/path/new-long-path
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let output = parse::parse_input(input)?;
|
||||||
|
|
||||||
|
pretty_assertions::assert_eq!(
|
||||||
|
Buffer {
|
||||||
|
files: vec![
|
||||||
|
File {
|
||||||
|
path: "/var/my".into(),
|
||||||
|
entry: FileEntry {
|
||||||
|
raw_op: Some("O".into()),
|
||||||
|
operation: Operation::Open {
|
||||||
|
index: Some("abc".into()),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
File {
|
||||||
|
path: "/var/my/path".into(),
|
||||||
|
entry: FileEntry {
|
||||||
|
raw_op: Some("OPEN".into()),
|
||||||
|
operation: Operation::Open {
|
||||||
|
index: Some("ecd".into())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
File {
|
||||||
|
path: "/var/my/path/new-path".into(),
|
||||||
|
entry: FileEntry {
|
||||||
|
raw_op: Some("O".into()),
|
||||||
|
operation: Operation::Open { index: None },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
File {
|
||||||
|
path: "/var/my/path/new-long-path".into(),
|
||||||
|
entry: FileEntry {
|
||||||
|
raw_op: Some("OPEN".into()),
|
||||||
|
operation: Operation::Open { index: None },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
output
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_parse_item_copy_operation() -> anyhow::Result<()> {
|
fn can_parse_item_copy_operation() -> anyhow::Result<()> {
|
||||||
let input = r#"
|
let input = r#"
|
||||||
|
Reference in New Issue
Block a user