8 Commits

Author SHA1 Message Date
1b2a35509c feat: move also creates parent dir on destination
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2025-08-24 17:44:17 +02:00
6129401cb5 chore(release): v0.1.2 (#11)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.1.2

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #11
2025-08-08 14:52:40 +02:00
1bc58a2047 feat: default to current dir if no file could be found
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-08 14:52:19 +02:00
de913cd375 chore: should be space instead
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-04 11:42:31 +02:00
d1b20ee5aa chore(release): v0.1.1 (#10)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.1.1

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #10
2025-08-04 11:12:16 +02:00
da00f41205 fix: if path is empty default to .
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-04 10:47:47 +02:00
f045ac5844 chore(release): v0.1.0 (#9)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.1.0

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #9
2025-08-03 17:01:02 +02:00
f8ba7ee744 feat: add open command
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2025-08-03 16:58:52 +02:00
5 changed files with 106 additions and 31 deletions

View File

@@ -6,9 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.1.2] - 2025-08-08
### Added
- default to current dir if no file could be found
### Other
- should be space instead
## [0.1.1] - 2025-08-04
### Fixed
- if path is empty default to `.`
## [0.1.0] - 2025-08-03
### Added
- add open command
- noil now handles open, and open in non-terminals via. /dev/tty
- add file opener with chooser
- update demo

2
Cargo.lock generated
View File

@@ -431,7 +431,7 @@ dependencies = [
[[package]]
name = "noil"
version = "0.0.5"
version = "0.1.1"
dependencies = [
"ansi_term",
"anyhow",

View File

@@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.1.0"
version = "0.1.2"
[workspace.dependencies]
noil = { path = "crates/noil" }

View File

@@ -1,8 +1,11 @@
# noil
**noil** is a structured, text-buffer-based file operation tool think of it like [`oil.nvim`](https://github.com/stevearc/oil.nvim), but for any editor, terminal, or automated process.
**noil** is a structured, text-buffer-based file operation tool think of it
like [`oil.nvim`](https://github.com/stevearc/oil.nvim), but for any editor,
terminal, or automated process.
Edit filesystem operations like it's plain text, and then apply them in a controlled, explicit way.
Edit filesystem operations like it's plain text, and then apply them in a
controlled, explicit way.
![demo](assets/demo.gif)
@@ -10,17 +13,19 @@ Edit filesystem operations like it's plain text, and then apply them in a contro
## Interactive Mode (TBA)
I am planning an interactive TUI mode, where you don't have to care about tags, like in `oil`. For now the normal editor is quite useful though, and allows all types of editors to easily move, edit files and so on.
I am planning an interactive TUI mode, where you don't have to care about tags,
like in `oil`. For now the normal editor is quite useful though, and allows all
types of editors to easily move, edit files and so on.
---
## ✨ Features
* Edit your file tree like a normal buffer
* Preview, format, and apply changes
* Integrates with `$EDITOR`
* CLI first, editor agnostic
* No surprises: nothing is applied until you say so
- Edit your file tree like a normal buffer
- Preview, format, and apply changes
- Integrates with `$EDITOR`
- CLI first, editor agnostic
- No surprises: nothing is applied until you say so
---
@@ -42,7 +47,7 @@ cat something.noil | noil fmt > something.noil
cat something.noil | noil apply
```
noil will always ask you if you want to apply your changes before doing any operations.
noil will ask you if you want to apply your changes before doing any operations.
---
@@ -56,13 +61,14 @@ Each line follows this format:
### Supported operations:
| Operation | Meaning | Tag Required? |
| --------: | --------------------------------- | ------------- |
| `ADD` | Add new file | ❌ No |
| `COPY` | Copy file with given tag | ✅ Yes |
| `DELETE` | Delete file with given tag | ✅ Yes |
| `MOVE` | Move file with given tag | ✅ Yes |
| *(blank)* | Reference existing file (default) | ✅ Yes |
| Operation | Meaning | Tag Required? |
| --------: | ------------------------------------------------------ | ------------- |
| `ADD` | Add new file | ❌ No |
| `COPY` | Copy file with given tag | ✅ Yes |
| `DELETE` | Delete file with given tag | ✅ Yes |
| `MOVE` | Move file with given tag | ✅ Yes |
| `OPEN` | Open a file with a given tag (requires --chooser-file) | ❌ No |
| _(blank)_ | Reference existing file (default) | ✅ Yes |
---
@@ -73,9 +79,11 @@ Each line follows this format:
COPY abc : /tmp/nginx-copy
DELETE 123 : /etc/nginx
ADD : /new/file.txt
OPEN : /new/file.txt
```
You can use short, unique tags (like `abc`, `ng1`, etc.) to refer to files. `noil` will generate these tags when you run `noil .`.
You can use short, unique tags (like `abc`, `ng1`, etc.) to refer to files.
`noil` will generate these tags when you run `noil .`.
---
@@ -87,9 +95,11 @@ Want to clean up alignment and spacing?
cat my-buffer.noil | noil fmt
```
Or automatically format inside your editor with the following config for [Helix](https://helix-editor.com):
Or automatically format inside your editor with the following config for
[Helix](https://helix-editor.com):
```toml
# .config/helix/languages.toml
[[language]]
name = "noil"
scope = "source.noil"
@@ -102,8 +112,30 @@ formatter = { command = "noil", args = ["fmt"] }
[[grammar]]
name = "noil"
source = { git = "https://git.kjuulh.io/kjuulh/tree-sitter-noil.git", rev = "2f295629439881d0b9e89108a1296881d0daf7b9" }
# .config/helix/config.toml
# Optional extra command Space + o will open noil allowing edits and the OPEN command
[keys.normal.space]
o = [
":sh rm -f /tmp/unique-file-kjuulh",
# DISCLAIMER: Until noil has a proper interactive mode, we cannot ask for confirmation, as such we always commit changes, you don't get to have a preview unlike the normal cli option
":insert-output noil edit '%{buffer_name}' --chooser-file=/tmp/unique-file-kjuulh --commit --quiet < /dev/tty",
":insert-output echo \"x1b[?1049h\" > /dev/tty",
":open %sh{cat /tmp/unique-file-kjuulh}",
":redraw",
]
```
### Edit options
When using `noil edit .` a few additional options are available
- `--chooser-file`: A chooser file is a newline delimited file where each line
corresponds to a relative file to be opened or manipulated by the user. Only
items with `OPEN` command will be added to the file
- `--commit`: commit files without asking for confirmation
- `--quiet`: don't print results
---
## 🔒 Safety First
@@ -123,7 +155,9 @@ You will be prompted before anything is modified.
## 🧠 Philosophy
noil gives you full control over file operations in a composable and editor-friendly way. Think Git index, but for actual file moves and deletions — human-editable, patchable, and grep-able.
noil gives you full control over file operations in a composable and
editor-friendly way. Think Git index, but for actual file moves and deletions —
human-editable, patchable, and grep-able.
---
@@ -135,7 +169,6 @@ noil gives you full control over file operations in a composable and editor-frie
cargo install noil
```
**Build from source**:
```bash

View File

@@ -40,10 +40,13 @@ pub struct EditCommand {
impl EditCommand {
pub async fn execute(&self) -> anyhow::Result<()> {
let mut small_id = Vec::with_capacity(8);
for id in small_id.iter_mut() {
*id = encode_rand::ALPHABET
[rand::random_range(0..(encode_rand::ALPHABET_LEN as u8)) as usize];
let mut small_id = Vec::new();
for _ in 0..8 {
small_id.push(
encode_rand::ALPHABET
[rand::random_range(0..(encode_rand::ALPHABET_LEN as u8)) as usize],
);
}
let small_id = String::from_utf8_lossy(&small_id);
@@ -62,9 +65,20 @@ impl EditCommand {
.await
.context("create temp file for noil")?;
let output = get_outputs(&self.get_path().await.context("get path")?, true)
let path = &self
.get_path()
.await
.context("get output")?;
.context("get path")
.inspect_err(|e| {
tracing::warn!(
"error: file path doesn't exist, defaulting to current working dir: {e}"
)
})
.unwrap_or_else(|_| PathBuf::from("."));
let output = get_outputs(path, true)
.await
.context(format!("get output: {}", path.display()))?;
file.write_all(output.as_bytes())
.await
.context("write contents for edit")?;
@@ -151,10 +165,16 @@ impl EditCommand {
}
if path.is_file() {
return path
let parent_path = path
.parent()
.map(|p| p.to_path_buf())
.ok_or(anyhow::anyhow!("parent doesn't exist for file"));
.ok_or(anyhow::anyhow!("parent doesn't exist for file"))?;
if parent_path.display().to_string() == "" {
return Ok(PathBuf::from("."));
}
return Ok(parent_path);
}
Ok(path.clone())
@@ -308,6 +328,14 @@ pub async fn apply(input: &str, options: ApplyOptions) -> anyhow::Result<()> {
anyhow::bail!("destination already exists cannot move");
}
if let Some(parent) = existing.path.parent()
&& !parent.exists()
{
tokio::fs::create_dir_all(&parent)
.await
.context("failed to create dest for move")?;
}
tokio::fs::rename(&existing.path, path)
.await
.context("move path")?;
@@ -334,7 +362,7 @@ pub async fn apply(input: &str, options: ApplyOptions) -> anyhow::Result<()> {
.map(|i| i.display().to_string())
.collect::<Vec<_>>();
file.write_all(open_files.join("\n").as_bytes())
file.write_all(open_files.join(" ").as_bytes())
.await
.context("write chosen files")?;
file.flush().await.context("flush chosen file")?;