feat: initial version of parts
This commit is contained in:
7 files changed
+1556
No files matched your search
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+411
@@ -0,0 +1,411 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "parts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"tempfile",
|
||||
"termion",
|
||||
"xdg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4648c7def6f2043b2568617b9f9b75eae88ca185dbc1f1fda30e95a85d49d7d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libredox",
|
||||
"numtoa",
|
||||
"redox_termios",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "parts"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Prepend and Review ToDo System"
|
||||
license = "MIT"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.41", features = ["derive"] }
|
||||
xdg = "3.0.0"
|
||||
termion = "2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.20.0"
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Stephen Waits <steve@waits.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,35 @@
|
||||
# PARTS - Prepend and Review ToDo System
|
||||
|
||||
A simple command-line productivity system that lets you add items to the top of your list and review them interactively.
|
||||
|
||||
## Inspiration
|
||||
|
||||
This tool was inspired by Andrej Karpathy's blog post ["The append-and-review note"](https://karpathy.bearblog.dev/the-append-and-review-note/), which describes a simple but effective productivity system using a single text file where you append new items to the top and periodically review from the top down.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository
|
||||
2. Build with Cargo:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
3. Copy the binary to your PATH:
|
||||
|
||||
```bash
|
||||
cp target/release/parts ~/.local/bin/
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
- `parts add "Your todo item"` - Add a new item to the top
|
||||
- `parts ls` - List your items
|
||||
- `parts scan` - Review items interactively
|
||||
- `parts do 1 2 3` - Archive completed items by number
|
||||
- `parts up 1 2 3` - Prioritize items by moving them to the top
|
||||
- `parts edit` - Edit your todo list in $EDITOR
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
+659
@@ -0,0 +1,659 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, BufRead, Read, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use termion::color;
|
||||
use termion::raw::IntoRawMode;
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "parts")]
|
||||
#[command(about = "Prepend and Review ToDo System", long_about = None)]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Add a new item at the top
|
||||
#[command(alias = "prepend")]
|
||||
Add {
|
||||
/// The text to add (e.g., "Read: Book XYZ")
|
||||
text: String,
|
||||
},
|
||||
/// List the top N items (default 5)
|
||||
#[command(alias = "list")]
|
||||
Ls {
|
||||
#[arg(short = 'n', long = "num", default_value_t = 5)]
|
||||
num: usize,
|
||||
/// Show all items (overrides -n/--num)
|
||||
#[arg(short = 'a', long = "all")]
|
||||
all: bool,
|
||||
/// Filter items containing this text (e.g., "read:", "@work")
|
||||
filter: Option<String>,
|
||||
},
|
||||
/// Raise the priority of items (move toward top)
|
||||
#[command(alias = "prioritize")]
|
||||
Up {
|
||||
/// Item numbers to prioritize (from "parts ls")
|
||||
numbers: Vec<usize>,
|
||||
},
|
||||
/// Archive completed items
|
||||
#[command(aliases = ["done", "finish", "check"])]
|
||||
Do {
|
||||
/// Item numbers to archive (from "parts ls")
|
||||
numbers: Vec<usize>,
|
||||
},
|
||||
/// Scan and review items interactively, from top to bottom
|
||||
#[command(alias = "review")]
|
||||
Scan,
|
||||
/// Edit items in your $EDITOR
|
||||
Edit,
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let base = BaseDirectories::with_prefix("parts");
|
||||
let note_path = base
|
||||
.place_data_file("note.txt")
|
||||
.expect("Failed to create data directory");
|
||||
let archive_path = base
|
||||
.place_data_file("archive.txt")
|
||||
.expect("Failed to create data directory");
|
||||
|
||||
match cli.command {
|
||||
Commands::Add { text } => add_entry(¬e_path, &text),
|
||||
Commands::Ls { num, all, filter } => list_note(¬e_path, num, all, filter.as_deref()),
|
||||
Commands::Up { numbers } => prioritize_items(¬e_path, &numbers),
|
||||
Commands::Do { numbers } => archive_items(¬e_path, &archive_path, &numbers),
|
||||
Commands::Scan => review_note(¬e_path, &archive_path),
|
||||
Commands::Edit => edit_note(¬e_path),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_entry(path: &Path, text: &str) -> io::Result<()> {
|
||||
let mut content = String::new();
|
||||
if path.exists() {
|
||||
content = fs::read_to_string(path)?;
|
||||
}
|
||||
let new_content = add_entry_to_content(&content, text);
|
||||
fs::write(path, new_content)
|
||||
}
|
||||
|
||||
fn add_entry_to_content(existing_content: &str, text: &str) -> String {
|
||||
format!("{}\n{}", text, existing_content.trim_end())
|
||||
}
|
||||
|
||||
fn list_note(path: &Path, num: usize, all: bool, filter: Option<&str>) -> io::Result<()> {
|
||||
if !path.exists() {
|
||||
println!(
|
||||
"{}No notes yet.{}",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let file = File::open(path)?;
|
||||
let lines: Vec<String> = io::BufReader::new(file).lines().collect::<Result<_, _>>()?;
|
||||
|
||||
let filtered_lines: Vec<(usize, &String)> = if let Some(filter_text) = filter {
|
||||
lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, line)| line.to_lowercase().contains(&filter_text.to_lowercase()))
|
||||
.collect()
|
||||
} else {
|
||||
lines.iter().enumerate().collect()
|
||||
};
|
||||
|
||||
if filtered_lines.is_empty() {
|
||||
if filter.is_some() {
|
||||
println!(
|
||||
"{}No items found matching filter: \"{}\"{}",
|
||||
color::Fg(color::Yellow),
|
||||
filter.unwrap(),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}No notes yet.{}",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let display_num = if all {
|
||||
filtered_lines.len()
|
||||
} else {
|
||||
num.min(filtered_lines.len())
|
||||
};
|
||||
for (original_idx, line) in filtered_lines.iter().take(display_num) {
|
||||
println!(
|
||||
"{}{}. {}{}",
|
||||
color::Fg(color::Blue),
|
||||
original_idx + 1,
|
||||
color::Fg(color::Reset),
|
||||
line
|
||||
);
|
||||
}
|
||||
|
||||
if !all && filter.is_some() && filtered_lines.len() > display_num {
|
||||
println!(
|
||||
"{}... and {} more matching items{}",
|
||||
color::Fg(color::Yellow),
|
||||
filtered_lines.len() - display_num,
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_key_input() -> io::Result<String> {
|
||||
let _stdout = io::stdout().into_raw_mode()?;
|
||||
let mut buffer = [0; 3];
|
||||
|
||||
// Read first byte
|
||||
io::stdin().read_exact(&mut buffer[0..1])?;
|
||||
|
||||
match buffer[0] {
|
||||
27 => {
|
||||
// ESC sequence, read next two bytes
|
||||
io::stdin().read_exact(&mut buffer[1..3])?;
|
||||
match (buffer[1], buffer[2]) {
|
||||
(91, 65) => Ok("up".to_string()), // ESC[A
|
||||
(91, 66) => Ok("down".to_string()), // ESC[B
|
||||
(91, 67) => Ok("right".to_string()), // ESC[C
|
||||
(91, 68) => Ok("left".to_string()), // ESC[D
|
||||
_ => Ok("unknown".to_string()),
|
||||
}
|
||||
}
|
||||
b'\n' | b'\r' => Ok("enter".to_string()),
|
||||
b'q' => Ok("q".to_string()),
|
||||
b'p' => Ok("p".to_string()),
|
||||
b'a' => Ok("a".to_string()),
|
||||
b's' => Ok("s".to_string()),
|
||||
_ => Ok("unknown".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn prioritize_items(note_path: &Path, numbers: &[usize]) -> io::Result<()> {
|
||||
if !note_path.exists() {
|
||||
println!(
|
||||
"{}No notes found.{}",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(note_path)?;
|
||||
let lines: Vec<String> = content.lines().map(str::to_string).collect();
|
||||
|
||||
if lines.is_empty() {
|
||||
println!(
|
||||
"{}No notes to prioritize.{}",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match prioritize_items_in_content(&lines, numbers) {
|
||||
Ok((new_content, prioritized_items)) => {
|
||||
fs::write(note_path, new_content)?;
|
||||
println!(
|
||||
"{}✓ Prioritized {} item(s){}",
|
||||
color::Fg(color::Green),
|
||||
prioritized_items.len(),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
for item in prioritized_items {
|
||||
println!(
|
||||
" {}{}{}",
|
||||
color::Fg(color::Magenta),
|
||||
item,
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(msg) => {
|
||||
println!(
|
||||
"{}{}{}",
|
||||
color::Fg(color::Red),
|
||||
msg,
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prioritize_items_in_content(
|
||||
lines: &[String],
|
||||
numbers: &[usize],
|
||||
) -> Result<(String, Vec<String>), String> {
|
||||
// Convert 1-based numbers to 0-based indices and validate
|
||||
let mut indices: Vec<usize> = Vec::new();
|
||||
for &num in numbers {
|
||||
if num == 0 || num > lines.len() {
|
||||
return Err(format!(
|
||||
"Invalid item number: {}. Valid range: 1-{}",
|
||||
num,
|
||||
lines.len()
|
||||
));
|
||||
}
|
||||
indices.push(num - 1);
|
||||
}
|
||||
|
||||
// Remove duplicates while preserving order
|
||||
let mut seen = HashSet::new();
|
||||
indices.retain(|&x| seen.insert(x));
|
||||
|
||||
// Extract items to prioritize in the order specified on command line
|
||||
let mut to_prioritize: Vec<String> = Vec::new();
|
||||
let indices_set: HashSet<usize> = indices.iter().cloned().collect();
|
||||
|
||||
for &idx in &indices {
|
||||
to_prioritize.push(lines[idx].clone());
|
||||
}
|
||||
|
||||
// Extract remaining items (preserving original order)
|
||||
let mut remaining: Vec<String> = Vec::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if !indices_set.contains(&i) {
|
||||
remaining.push(line.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Combine: prioritized items first (in command-line order), then remaining
|
||||
let mut new_lines = to_prioritize.clone();
|
||||
new_lines.extend(remaining);
|
||||
|
||||
let new_content = if new_lines.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
new_lines.join("\n") + "\n"
|
||||
};
|
||||
|
||||
Ok((new_content, to_prioritize))
|
||||
}
|
||||
|
||||
fn archive_items(note_path: &Path, archive_path: &Path, numbers: &[usize]) -> io::Result<()> {
|
||||
if !note_path.exists() {
|
||||
println!(
|
||||
"{}No notes found.{}",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(note_path)?;
|
||||
let lines: Vec<String> = content.lines().map(str::to_string).collect();
|
||||
|
||||
if lines.is_empty() {
|
||||
println!(
|
||||
"{}No notes to archive.{}",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match archive_items_in_content(&lines, numbers) {
|
||||
Ok((new_content, archived_items)) => {
|
||||
// Archive items
|
||||
if !archived_items.is_empty() {
|
||||
let mut archive_file = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(archive_path)?;
|
||||
for item in &archived_items {
|
||||
writeln!(archive_file, "{item}")?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(note_path, new_content)?;
|
||||
println!(
|
||||
"{}✓ Archived {} item(s){}",
|
||||
color::Fg(color::Green),
|
||||
archived_items.len(),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
for item in archived_items {
|
||||
println!(
|
||||
" {}{}{}",
|
||||
color::Fg(color::Red),
|
||||
item,
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(msg) => {
|
||||
println!(
|
||||
"{}{}{}",
|
||||
color::Fg(color::Red),
|
||||
msg,
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn archive_items_in_content(
|
||||
lines: &[String],
|
||||
numbers: &[usize],
|
||||
) -> Result<(String, Vec<String>), String> {
|
||||
// Convert 1-based numbers to 0-based indices and validate
|
||||
let mut indices: Vec<usize> = Vec::new();
|
||||
for &num in numbers {
|
||||
if num == 0 || num > lines.len() {
|
||||
return Err(format!(
|
||||
"Invalid item number: {}. Valid range: 1-{}",
|
||||
num,
|
||||
lines.len()
|
||||
));
|
||||
}
|
||||
indices.push(num - 1);
|
||||
}
|
||||
|
||||
// Remove duplicates and sort
|
||||
indices.sort_unstable();
|
||||
indices.dedup();
|
||||
|
||||
// Extract items to archive and remaining items
|
||||
let mut to_archive: Vec<String> = Vec::new();
|
||||
let mut remaining: Vec<String> = Vec::new();
|
||||
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if indices.contains(&i) {
|
||||
to_archive.push(line.clone());
|
||||
} else {
|
||||
remaining.push(line.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let new_content = if remaining.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
remaining.join("\n") + "\n"
|
||||
};
|
||||
|
||||
Ok((new_content, to_archive))
|
||||
}
|
||||
|
||||
fn review_note(note_path: &Path, archive_path: &Path) -> io::Result<()> {
|
||||
if !note_path.exists() {
|
||||
println!("No notes to review.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(note_path)?;
|
||||
let lines: Vec<String> = content.lines().map(str::to_string).collect();
|
||||
let total = lines.len();
|
||||
|
||||
if total == 0 {
|
||||
println!("No notes to review.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut prioritized: Vec<String> = Vec::new();
|
||||
let mut to_archive: Vec<String> = Vec::new();
|
||||
let mut remove_indices: HashSet<usize> = HashSet::new();
|
||||
let mut history: Vec<(usize, String, String)> = Vec::new(); // (index, action, item)
|
||||
|
||||
let mut current_idx = 0;
|
||||
let indices: Vec<usize> = (0..total).collect();
|
||||
|
||||
while current_idx < indices.len() {
|
||||
let i = indices[current_idx];
|
||||
let current = current_idx + 1;
|
||||
|
||||
// Clear screen using ANSI escape sequence
|
||||
print!("\x1B[2J\x1B[H");
|
||||
|
||||
println!(
|
||||
"{}=== PARTS Review Mode ==={}",
|
||||
color::Fg(color::Cyan),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!(
|
||||
"{}Progress: {}/{} items remaining{}",
|
||||
color::Fg(color::Yellow),
|
||||
current,
|
||||
total,
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
"{}Item:{} {}",
|
||||
color::Fg(color::Green),
|
||||
color::Fg(color::Reset),
|
||||
lines[i]
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
"{}Actions:{}",
|
||||
color::Fg(color::Blue),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!(
|
||||
" {}[↑/p]{} Prioritize (move to top)",
|
||||
color::Fg(color::Magenta),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!(
|
||||
" {}[↓/a]{} Archive (remove from list)",
|
||||
color::Fg(color::Red),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!(
|
||||
" {}[→/Enter/s]{} Skip (keep in current position)",
|
||||
color::Fg(color::White),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!(
|
||||
" {}[←]{} Go back to previous item",
|
||||
color::Fg(color::Cyan),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!(
|
||||
" {}[q]{} Quit review",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!();
|
||||
print!(
|
||||
"{}Choose action:{} ",
|
||||
color::Fg(color::Blue),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
|
||||
// Read input (handle arrow keys)
|
||||
let action = read_key_input()?;
|
||||
|
||||
match action.as_str() {
|
||||
"up" | "p" => {
|
||||
history.push((i, "prioritize".to_string(), lines[i].clone()));
|
||||
prioritized.push(lines[i].clone());
|
||||
remove_indices.insert(i);
|
||||
println!(
|
||||
"↑\n{}✓ Prioritized{}",
|
||||
color::Fg(color::Magenta),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
current_idx += 1;
|
||||
}
|
||||
"down" | "a" => {
|
||||
history.push((i, "archive".to_string(), lines[i].clone()));
|
||||
to_archive.push(lines[i].clone());
|
||||
remove_indices.insert(i);
|
||||
println!(
|
||||
"↓\n{}✓ Archived{}",
|
||||
color::Fg(color::Red),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
current_idx += 1;
|
||||
}
|
||||
"right" | "enter" | "s" => {
|
||||
history.push((i, "skip".to_string(), lines[i].clone()));
|
||||
println!(
|
||||
"→\n{}✓ Skipped{}",
|
||||
color::Fg(color::White),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
current_idx += 1;
|
||||
}
|
||||
"left" => {
|
||||
if let Some((prev_idx, prev_action, prev_item)) = history.pop() {
|
||||
// Undo the previous action
|
||||
match prev_action.as_str() {
|
||||
"prioritize" => {
|
||||
prioritized.retain(|item| item != &prev_item);
|
||||
remove_indices.remove(&prev_idx);
|
||||
}
|
||||
"archive" => {
|
||||
to_archive.retain(|item| item != &prev_item);
|
||||
remove_indices.remove(&prev_idx);
|
||||
}
|
||||
"skip" => {
|
||||
// Nothing to undo for skip
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
current_idx = current_idx.saturating_sub(1);
|
||||
println!(
|
||||
"←\n{}✓ Went back{}",
|
||||
color::Fg(color::Cyan),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"←\n{}No previous item to go back to{}",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
}
|
||||
}
|
||||
"q" => {
|
||||
println!(
|
||||
"q\n{}Quitting review and saving changes...{}",
|
||||
color::Fg(color::Yellow),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
history.push((i, "skip".to_string(), lines[i].clone()));
|
||||
println!(
|
||||
"?\n{}✓ Skipped (invalid key){}",
|
||||
color::Fg(color::White),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
current_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear screen for final summary
|
||||
print!("\x1B[2J\x1B[H");
|
||||
println!(
|
||||
"{}=== Review Complete ==={}",
|
||||
color::Fg(color::Green),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
println!(
|
||||
"{}Prioritized:{} {} items",
|
||||
color::Fg(color::Magenta),
|
||||
color::Fg(color::Reset),
|
||||
prioritized.len()
|
||||
);
|
||||
println!(
|
||||
"{}Archived:{} {} items",
|
||||
color::Fg(color::Red),
|
||||
color::Fg(color::Reset),
|
||||
to_archive.len()
|
||||
);
|
||||
println!(
|
||||
"{}Remaining:{} {} items",
|
||||
color::Fg(color::Blue),
|
||||
color::Fg(color::Reset),
|
||||
total - remove_indices.len()
|
||||
);
|
||||
println!();
|
||||
|
||||
// Archive items
|
||||
if !to_archive.is_empty() {
|
||||
let mut archive_file = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(archive_path)?;
|
||||
for arch in to_archive {
|
||||
writeln!(archive_file, "{arch}")?;
|
||||
}
|
||||
}
|
||||
|
||||
// Build remaining items
|
||||
let mut remaining: Vec<String> = Vec::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if !remove_indices.contains(&i) {
|
||||
remaining.push(line.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Combine: prioritized items first, then remaining
|
||||
let mut new_lines = prioritized;
|
||||
new_lines.extend(remaining);
|
||||
|
||||
// Write back to file
|
||||
let new_content = if new_lines.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
new_lines.join("\n") + "\n"
|
||||
};
|
||||
fs::write(note_path, new_content)?;
|
||||
|
||||
println!(
|
||||
"{}✓ Changes saved successfully!{}",
|
||||
color::Fg(color::Green),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn edit_note(note_path: &Path) -> io::Result<()> {
|
||||
let editor = env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
|
||||
|
||||
// Create the file if it doesn't exist
|
||||
if !note_path.exists() {
|
||||
fs::write(note_path, "")?;
|
||||
}
|
||||
|
||||
let status = Command::new(&editor).arg(note_path).status()?;
|
||||
|
||||
if !status.success() {
|
||||
println!(
|
||||
"{}Editor exited with error code: {}{}",
|
||||
color::Fg(color::Red),
|
||||
status.code().unwrap_or(-1),
|
||||
color::Fg(color::Reset)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add_entry_to_content_empty() {
|
||||
let result = add_entry_to_content("", "First item");
|
||||
assert_eq!(result, "First item\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_entry_to_content_existing() {
|
||||
let existing = "Second item\nThird item";
|
||||
let result = add_entry_to_content(existing, "First item");
|
||||
assert_eq!(result, "First item\nSecond item\nThird item");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_entry_to_content_with_trailing_newline() {
|
||||
let existing = "Second item\nThird item\n";
|
||||
let result = add_entry_to_content(existing, "First item");
|
||||
assert_eq!(result, "First item\nSecond item\nThird item");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prioritize_items_in_content_single_item() {
|
||||
let lines = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
];
|
||||
let result = prioritize_items_in_content(&lines, &[2]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, prioritized) = result.unwrap();
|
||||
assert_eq!(new_content, "Second item\nFirst item\nThird item\n");
|
||||
assert_eq!(prioritized, vec!["Second item"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prioritize_items_in_content_multiple_items() {
|
||||
let lines = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
"Fourth item".to_string(),
|
||||
];
|
||||
let result = prioritize_items_in_content(&lines, &[3, 1]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, prioritized) = result.unwrap();
|
||||
assert_eq!(
|
||||
new_content,
|
||||
"Third item\nFirst item\nSecond item\nFourth item\n"
|
||||
);
|
||||
assert_eq!(prioritized, vec!["Third item", "First item"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prioritize_items_in_content_invalid_number() {
|
||||
let lines = vec!["First item".to_string(), "Second item".to_string()];
|
||||
let result = prioritize_items_in_content(&lines, &[3]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
"Invalid item number: 3. Valid range: 1-2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prioritize_items_in_content_zero_number() {
|
||||
let lines = vec!["First item".to_string()];
|
||||
let result = prioritize_items_in_content(&lines, &[0]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
"Invalid item number: 0. Valid range: 1-1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prioritize_items_in_content_duplicates() {
|
||||
let lines = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
];
|
||||
let result = prioritize_items_in_content(&lines, &[2, 2, 1]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, prioritized) = result.unwrap();
|
||||
assert_eq!(new_content, "Second item\nFirst item\nThird item\n");
|
||||
assert_eq!(prioritized, vec!["Second item", "First item"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_archive_items_in_content_single_item() {
|
||||
let lines = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
];
|
||||
let result = archive_items_in_content(&lines, &[2]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, archived) = result.unwrap();
|
||||
assert_eq!(new_content, "First item\nThird item\n");
|
||||
assert_eq!(archived, vec!["Second item"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_archive_items_in_content_multiple_items() {
|
||||
let lines = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
"Fourth item".to_string(),
|
||||
];
|
||||
let result = archive_items_in_content(&lines, &[1, 3]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, archived) = result.unwrap();
|
||||
assert_eq!(new_content, "Second item\nFourth item\n");
|
||||
assert_eq!(archived, vec!["First item", "Third item"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_archive_items_in_content_all_items() {
|
||||
let lines = vec!["First item".to_string(), "Second item".to_string()];
|
||||
let result = archive_items_in_content(&lines, &[1, 2]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, archived) = result.unwrap();
|
||||
assert_eq!(new_content, "");
|
||||
assert_eq!(archived, vec!["First item", "Second item"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_archive_items_in_content_invalid_number() {
|
||||
let lines = vec!["First item".to_string()];
|
||||
let result = archive_items_in_content(&lines, &[2]);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
"Invalid item number: 2. Valid range: 1-1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_archive_items_in_content_duplicates() {
|
||||
let lines = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
];
|
||||
let result = archive_items_in_content(&lines, &[1, 1, 3]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, archived) = result.unwrap();
|
||||
assert_eq!(new_content, "Second item\n");
|
||||
assert_eq!(archived, vec!["First item", "Third item"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_functionality() {
|
||||
let lines = vec![
|
||||
"Read: Book about Rust".to_string(),
|
||||
"Listen: Podcast episode".to_string(),
|
||||
"@work: Review PR".to_string(),
|
||||
"@home: Fix sink".to_string(),
|
||||
"Read: Another book".to_string(),
|
||||
];
|
||||
|
||||
// Test filtering by "read:"
|
||||
let filtered: Vec<(usize, &String)> = lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, line)| line.to_lowercase().contains("read:"))
|
||||
.collect();
|
||||
|
||||
assert_eq!(filtered.len(), 2);
|
||||
assert_eq!(filtered[0].0, 0);
|
||||
assert_eq!(filtered[0].1, "Read: Book about Rust");
|
||||
assert_eq!(filtered[1].0, 4);
|
||||
assert_eq!(filtered[1].1, "Read: Another book");
|
||||
|
||||
// Test filtering by "@work"
|
||||
let filtered: Vec<(usize, &String)> = lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, line)| line.to_lowercase().contains("@work"))
|
||||
.collect();
|
||||
|
||||
assert_eq!(filtered.len(), 1);
|
||||
assert_eq!(filtered[0].0, 2);
|
||||
assert_eq!(filtered[0].1, "@work: Review PR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_insensitive_filtering() {
|
||||
let lines = vec![
|
||||
"READ: Book about Rust".to_string(),
|
||||
"read: Another book".to_string(),
|
||||
"ReAd: Mixed case".to_string(),
|
||||
];
|
||||
|
||||
let filtered: Vec<(usize, &String)> = lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, line)| line.to_lowercase().contains("read:"))
|
||||
.collect();
|
||||
|
||||
assert_eq!(filtered.len(), 3);
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod list_tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
fn create_test_file_with_content(content: &str) -> NamedTempFile {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
write!(file, "{}", content).unwrap();
|
||||
file
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_with_all_flag() {
|
||||
let content = "item 1\nitem 2\nitem 3\nitem 4\nitem 5\nitem 6";
|
||||
let file = create_test_file_with_content(content);
|
||||
|
||||
// Test that all=true shows all items regardless of num
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
list_note(file.path(), 3, true, None).unwrap();
|
||||
});
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_without_all_flag() {
|
||||
let content = "item 1\nitem 2\nitem 3\nitem 4\nitem 5\nitem 6";
|
||||
let file = create_test_file_with_content(content);
|
||||
|
||||
// Test that all=false respects num limit
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
list_note(file.path(), 3, false, None).unwrap();
|
||||
});
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_with_filter_and_all() {
|
||||
let content = "read: book 1\nwrite: article\nread: book 2\ntask: cleanup";
|
||||
let file = create_test_file_with_content(content);
|
||||
|
||||
// Test that all=true with filter shows all matching items
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
list_note(file.path(), 1, true, Some("read:")).unwrap();
|
||||
});
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_empty_file() {
|
||||
let file = create_test_file_with_content("");
|
||||
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
list_note(file.path(), 5, false, None).unwrap();
|
||||
});
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod review_tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_review_prioritization_order() {
|
||||
// Test that items prioritized during review maintain their relative order
|
||||
let lines = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
"Fourth item".to_string(),
|
||||
];
|
||||
|
||||
// Simulate review process: prioritize items 2 and 4 (in that encounter order during bottom-to-top review)
|
||||
let mut prioritized: Vec<String> = Vec::new();
|
||||
let mut remove_indices: HashSet<usize> = HashSet::new();
|
||||
|
||||
// Simulate top-to-bottom review order (indices 0, 1, 2, 3) - same as list order
|
||||
// User prioritizes item at index 1 (Second item)
|
||||
prioritized.push(lines[1].clone());
|
||||
remove_indices.insert(1);
|
||||
|
||||
// User prioritizes item at index 3 (Fourth item)
|
||||
prioritized.push(lines[3].clone());
|
||||
remove_indices.insert(3);
|
||||
|
||||
// Build remaining items (items not prioritized)
|
||||
let mut remaining: Vec<String> = Vec::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if !remove_indices.contains(&i) {
|
||||
remaining.push(line.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Combine: prioritized items first (in order they were prioritized), then remaining
|
||||
let mut new_lines = prioritized;
|
||||
new_lines.extend(remaining);
|
||||
|
||||
// Expected result: prioritized items (Second, Fourth) should appear first,
|
||||
// in the order they were prioritized, followed by remaining items (First, Third)
|
||||
let expected = vec![
|
||||
"Second item".to_string(),
|
||||
"Fourth item".to_string(),
|
||||
"First item".to_string(),
|
||||
"Third item".to_string(),
|
||||
];
|
||||
|
||||
assert_eq!(new_lines, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_review_quit_saves_changes() {
|
||||
// Test that quitting review still saves any changes made
|
||||
let lines = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
];
|
||||
|
||||
// Simulate review process where user prioritizes one item then quits
|
||||
let mut prioritized: Vec<String> = Vec::new();
|
||||
let mut remove_indices: HashSet<usize> = HashSet::new();
|
||||
|
||||
// User prioritizes item at index 1 (Second item)
|
||||
prioritized.push(lines[1].clone());
|
||||
remove_indices.insert(1);
|
||||
|
||||
// User quits (but changes should still be applied)
|
||||
|
||||
// Build remaining items (items not prioritized or archived)
|
||||
let mut remaining: Vec<String> = Vec::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if !remove_indices.contains(&i) {
|
||||
remaining.push(line.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Combine: prioritized items first, then remaining
|
||||
let mut new_lines = prioritized;
|
||||
new_lines.extend(remaining);
|
||||
|
||||
// Expected result: prioritized item should be first, even though user quit
|
||||
let expected = vec![
|
||||
"Second item".to_string(),
|
||||
"First item".to_string(),
|
||||
"Third item".to_string(),
|
||||
];
|
||||
|
||||
assert_eq!(new_lines, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prioritize_command_line_order() {
|
||||
// Test that items are prioritized in the exact order specified on command line
|
||||
let lines = vec![
|
||||
"Item A".to_string(),
|
||||
"Item B".to_string(),
|
||||
"Item C".to_string(),
|
||||
"Item D".to_string(),
|
||||
"Item E".to_string(),
|
||||
];
|
||||
|
||||
// Command: arn up 5 2 4 (prioritize items 5, 2, 4 in that order)
|
||||
let result = super::prioritize_items_in_content(&lines, &[5, 2, 4]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, prioritized) = result.unwrap();
|
||||
|
||||
// Expected: Item E, Item B, Item D should be at top in that order
|
||||
let expected_content = "Item E\nItem B\nItem D\nItem A\nItem C\n";
|
||||
let expected_prioritized = vec!["Item E", "Item B", "Item D"];
|
||||
|
||||
assert_eq!(new_content, expected_content);
|
||||
assert_eq!(prioritized, expected_prioritized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prioritize_command_line_order_with_duplicates() {
|
||||
// Test that duplicates are removed but order is preserved
|
||||
let lines = vec![
|
||||
"Item A".to_string(),
|
||||
"Item B".to_string(),
|
||||
"Item C".to_string(),
|
||||
];
|
||||
|
||||
// Command: arn up 3 1 3 2 1 (should become 3, 1, 2)
|
||||
let result = super::prioritize_items_in_content(&lines, &[3, 1, 3, 2, 1]);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (new_content, prioritized) = result.unwrap();
|
||||
|
||||
// Expected: Item C, Item A, Item B (duplicates removed, order preserved)
|
||||
let expected_content = "Item C\nItem A\nItem B\n";
|
||||
let expected_prioritized = vec!["Item C", "Item A", "Item B"];
|
||||
|
||||
assert_eq!(new_content, expected_content);
|
||||
assert_eq!(prioritized, expected_prioritized);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user