├── .github └── workflows │ ├── cargo-build.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── NOTES.md ├── README.md ├── sigi.1 ├── src ├── bin │ └── sigi.rs ├── cli.rs ├── cli │ └── interact.rs ├── data.rs ├── effects.rs ├── lib.rs └── output.rs └── tests ├── abcd_tests.rs ├── basic_sigi_tests.rs ├── empty_stack_tests.rs ├── interactive_tests.rs ├── run_sigi.rs └── single_item_tests.rs /.github/workflows/cargo-build.yml: -------------------------------------------------------------------------------- 1 | name: cargo build 2 | 3 | on: 4 | push: 5 | branches: [ core ] 6 | pull_request: 7 | branches: [ core ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: snickerbockers/submodules-init@v4 20 | - name: Build 21 | run: cargo build --verbose 22 | - name: Run tests 23 | run: cargo test --verbose 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Add artifacts to release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release: 10 | name: release ${{ matrix.target }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - target: x86_64-unknown-linux-musl 17 | - target: x86_64-pc-windows-gnu 18 | # TODO: How? 19 | # - target: x86_64-unknown-linux-gnu 20 | # - target: aarch64-unknown-linux-musl 21 | steps: 22 | - uses: actions/checkout@master 23 | - name: Compile and release 24 | uses: rust-build/rust-build.action@v1.4.3 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | with: 28 | RUSTTARGET: ${{ matrix.target }} 29 | TOOLCHAIN_VERSION: 1.78.0 30 | ARCHIVE_TYPES: tar.gz zip 31 | EXTRA_FILES: LICENSE README.md sigi.1 32 | 33 | release-macos: 34 | name: release ${{ matrix.target }} 35 | runs-on: macos-latest 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | include: 40 | - target: aarch64-apple-darwin 41 | - target: x86_64-apple-darwin 42 | env: 43 | ASSET_NAME: sigi_${{ github.event.release.tag_name }}_${{ matrix.target }} 44 | RELEASE_URL: https://uploads.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets 45 | steps: 46 | - uses: actions/checkout@master 47 | - run: rustup update stable && rustup default stable 48 | - run: rustup target add ${{ matrix.target }} 49 | - run: cargo build --target ${{ matrix.target }} --release --verbose 50 | - run: cargo test --target ${{ matrix.target }} --release --verbose 51 | - run: mv target/${{ matrix.target }}/release/sigi sigi 52 | - run: zip ${{ env.ASSET_NAME }}.zip sigi LICENSE README.md sigi.1 53 | - run: shasum -a 256 ${{ env.ASSET_NAME }}.zip >${{ env.ASSET_NAME }}.zip.sha256sum 54 | - run: tar -czvf ${{ env.ASSET_NAME }}.tar.gz sigi LICENSE README.md sigi.1 55 | - run: shasum -a 256 ${{ env.ASSET_NAME }}.tar.gz >${{ env.ASSET_NAME }}.tar.gz.sha256sum 56 | 57 | # https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset 58 | - name: upload zip 59 | run: | 60 | curl -L \ 61 | -H "Accept: application/vnd.github+json" \ 62 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ 63 | -H "X-GitHub-Api-Version: 2022-11-28" \ 64 | -H "Content-Type: application/octet-stream" \ 65 | "${{ env.RELEASE_URL}}?name=${{ env.ASSET_NAME }}.zip" \ 66 | --data-binary "@${{ env.ASSET_NAME }}.zip" 67 | - name: upload zip sha256sum 68 | run: | 69 | curl -L \ 70 | -H "Accept: application/vnd.github+json" \ 71 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ 72 | -H "X-GitHub-Api-Version: 2022-11-28" \ 73 | -H "Content-Type: application/octet-stream" \ 74 | "${{ env.RELEASE_URL}}?name=${{ env.ASSET_NAME }}.zip.sha256sum" \ 75 | --data-binary "@${{ env.ASSET_NAME }}.zip.sha256sum" 76 | - name: upload tarball 77 | run: | 78 | curl -L \ 79 | -H "Accept: application/vnd.github+json" \ 80 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ 81 | -H "X-GitHub-Api-Version: 2022-11-28" \ 82 | -H "Content-Type: application/octet-stream" \ 83 | "${{ env.RELEASE_URL}}?name=${{ env.ASSET_NAME }}.tar.gz" \ 84 | --data-binary "@${{ env.ASSET_NAME }}.tar.gz" 85 | - name: upload tarball sha256sum 86 | run: | 87 | curl -L \ 88 | -H "Accept: application/vnd.github+json" \ 89 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ 90 | -H "X-GitHub-Api-Version: 2022-11-28" \ 91 | -H "Content-Type: application/octet-stream" \ 92 | "${{ env.RELEASE_URL}}?name=${{ env.ASSET_NAME }}.tar.gz.sha256sum" \ 93 | --data-binary "@${{ env.ASSET_NAME }}.tar.gz.sha256sum" 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys 0.59.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell", 67 | "windows-sys 0.59.0", 68 | ] 69 | 70 | [[package]] 71 | name = "autocfg" 72 | version = "1.4.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 75 | 76 | [[package]] 77 | name = "bitflags" 78 | version = "1.3.2" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 81 | 82 | [[package]] 83 | name = "bitflags" 84 | version = "2.9.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 87 | 88 | [[package]] 89 | name = "bumpalo" 90 | version = "3.17.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 93 | 94 | [[package]] 95 | name = "cc" 96 | version = "1.2.22" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" 99 | dependencies = [ 100 | "shlex", 101 | ] 102 | 103 | [[package]] 104 | name = "cfg-if" 105 | version = "1.0.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 108 | 109 | [[package]] 110 | name = "chrono" 111 | version = "0.4.41" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 114 | dependencies = [ 115 | "android-tzdata", 116 | "iana-time-zone", 117 | "js-sys", 118 | "num-traits", 119 | "pure-rust-locales", 120 | "serde", 121 | "wasm-bindgen", 122 | "windows-link", 123 | ] 124 | 125 | [[package]] 126 | name = "clap" 127 | version = "4.5.38" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 130 | dependencies = [ 131 | "clap_builder", 132 | "clap_derive", 133 | ] 134 | 135 | [[package]] 136 | name = "clap_builder" 137 | version = "4.5.38" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 140 | dependencies = [ 141 | "anstream", 142 | "anstyle", 143 | "clap_lex", 144 | "strsim", 145 | ] 146 | 147 | [[package]] 148 | name = "clap_derive" 149 | version = "4.5.32" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 152 | dependencies = [ 153 | "heck", 154 | "proc-macro2", 155 | "quote", 156 | "syn", 157 | ] 158 | 159 | [[package]] 160 | name = "clap_lex" 161 | version = "0.7.4" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 164 | 165 | [[package]] 166 | name = "clearscreen" 167 | version = "2.0.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "72f3f22f1a586604e62efd23f78218f3ccdecf7a33c4500db2d37d85a24fe994" 170 | dependencies = [ 171 | "nix", 172 | "terminfo", 173 | "thiserror", 174 | "which", 175 | "winapi", 176 | ] 177 | 178 | [[package]] 179 | name = "clipboard-win" 180 | version = "4.5.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" 183 | dependencies = [ 184 | "error-code", 185 | "str-buf", 186 | "winapi", 187 | ] 188 | 189 | [[package]] 190 | name = "colorchoice" 191 | version = "1.0.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 194 | 195 | [[package]] 196 | name = "core-foundation-sys" 197 | version = "0.8.7" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 200 | 201 | [[package]] 202 | name = "directories" 203 | version = "5.0.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 206 | dependencies = [ 207 | "dirs-sys 0.4.1", 208 | ] 209 | 210 | [[package]] 211 | name = "dirs" 212 | version = "4.0.0" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 215 | dependencies = [ 216 | "dirs-sys 0.3.7", 217 | ] 218 | 219 | [[package]] 220 | name = "dirs-sys" 221 | version = "0.3.7" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 224 | dependencies = [ 225 | "libc", 226 | "redox_users", 227 | "winapi", 228 | ] 229 | 230 | [[package]] 231 | name = "dirs-sys" 232 | version = "0.4.1" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 235 | dependencies = [ 236 | "libc", 237 | "option-ext", 238 | "redox_users", 239 | "windows-sys 0.48.0", 240 | ] 241 | 242 | [[package]] 243 | name = "either" 244 | version = "1.15.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 247 | 248 | [[package]] 249 | name = "endian-type" 250 | version = "0.1.2" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 253 | 254 | [[package]] 255 | name = "errno" 256 | version = "0.3.12" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 259 | dependencies = [ 260 | "libc", 261 | "windows-sys 0.59.0", 262 | ] 263 | 264 | [[package]] 265 | name = "error-code" 266 | version = "2.3.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" 269 | dependencies = [ 270 | "libc", 271 | "str-buf", 272 | ] 273 | 274 | [[package]] 275 | name = "fd-lock" 276 | version = "3.0.13" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" 279 | dependencies = [ 280 | "cfg-if", 281 | "rustix", 282 | "windows-sys 0.48.0", 283 | ] 284 | 285 | [[package]] 286 | name = "fnv" 287 | version = "1.0.7" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 290 | 291 | [[package]] 292 | name = "getrandom" 293 | version = "0.2.16" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 296 | dependencies = [ 297 | "cfg-if", 298 | "libc", 299 | "wasi", 300 | ] 301 | 302 | [[package]] 303 | name = "heck" 304 | version = "0.5.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 307 | 308 | [[package]] 309 | name = "home" 310 | version = "0.5.11" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 313 | dependencies = [ 314 | "windows-sys 0.59.0", 315 | ] 316 | 317 | [[package]] 318 | name = "iana-time-zone" 319 | version = "0.1.63" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 322 | dependencies = [ 323 | "android_system_properties", 324 | "core-foundation-sys", 325 | "iana-time-zone-haiku", 326 | "js-sys", 327 | "log", 328 | "wasm-bindgen", 329 | "windows-core", 330 | ] 331 | 332 | [[package]] 333 | name = "iana-time-zone-haiku" 334 | version = "0.1.2" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 337 | dependencies = [ 338 | "cc", 339 | ] 340 | 341 | [[package]] 342 | name = "is_terminal_polyfill" 343 | version = "1.70.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 346 | 347 | [[package]] 348 | name = "itoa" 349 | version = "1.0.15" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 352 | 353 | [[package]] 354 | name = "js-sys" 355 | version = "0.3.77" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 358 | dependencies = [ 359 | "once_cell", 360 | "wasm-bindgen", 361 | ] 362 | 363 | [[package]] 364 | name = "json" 365 | version = "0.12.4" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" 368 | 369 | [[package]] 370 | name = "libc" 371 | version = "0.2.172" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 374 | 375 | [[package]] 376 | name = "libredox" 377 | version = "0.1.3" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 380 | dependencies = [ 381 | "bitflags 2.9.1", 382 | "libc", 383 | ] 384 | 385 | [[package]] 386 | name = "linux-raw-sys" 387 | version = "0.4.15" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 390 | 391 | [[package]] 392 | name = "log" 393 | version = "0.4.27" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 396 | 397 | [[package]] 398 | name = "memchr" 399 | version = "2.7.4" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 402 | 403 | [[package]] 404 | name = "minimal-lexical" 405 | version = "0.2.1" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 408 | 409 | [[package]] 410 | name = "nibble_vec" 411 | version = "0.1.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 414 | dependencies = [ 415 | "smallvec", 416 | ] 417 | 418 | [[package]] 419 | name = "nix" 420 | version = "0.26.4" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 423 | dependencies = [ 424 | "bitflags 1.3.2", 425 | "cfg-if", 426 | "libc", 427 | ] 428 | 429 | [[package]] 430 | name = "nom" 431 | version = "7.1.3" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 434 | dependencies = [ 435 | "memchr", 436 | "minimal-lexical", 437 | ] 438 | 439 | [[package]] 440 | name = "num-traits" 441 | version = "0.2.19" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 444 | dependencies = [ 445 | "autocfg", 446 | ] 447 | 448 | [[package]] 449 | name = "once_cell" 450 | version = "1.21.3" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 453 | 454 | [[package]] 455 | name = "option-ext" 456 | version = "0.2.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 459 | 460 | [[package]] 461 | name = "phf" 462 | version = "0.11.3" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 465 | dependencies = [ 466 | "phf_shared", 467 | ] 468 | 469 | [[package]] 470 | name = "phf_codegen" 471 | version = "0.11.3" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 474 | dependencies = [ 475 | "phf_generator", 476 | "phf_shared", 477 | ] 478 | 479 | [[package]] 480 | name = "phf_generator" 481 | version = "0.11.3" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 484 | dependencies = [ 485 | "phf_shared", 486 | "rand", 487 | ] 488 | 489 | [[package]] 490 | name = "phf_shared" 491 | version = "0.11.3" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 494 | dependencies = [ 495 | "siphasher", 496 | ] 497 | 498 | [[package]] 499 | name = "proc-macro2" 500 | version = "1.0.95" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 503 | dependencies = [ 504 | "unicode-ident", 505 | ] 506 | 507 | [[package]] 508 | name = "pure-rust-locales" 509 | version = "0.8.1" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "1190fd18ae6ce9e137184f207593877e70f39b015040156b1e05081cdfe3733a" 512 | 513 | [[package]] 514 | name = "quote" 515 | version = "1.0.40" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 518 | dependencies = [ 519 | "proc-macro2", 520 | ] 521 | 522 | [[package]] 523 | name = "radix_trie" 524 | version = "0.2.1" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 527 | dependencies = [ 528 | "endian-type", 529 | "nibble_vec", 530 | ] 531 | 532 | [[package]] 533 | name = "rand" 534 | version = "0.8.5" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 537 | dependencies = [ 538 | "rand_core", 539 | ] 540 | 541 | [[package]] 542 | name = "rand_core" 543 | version = "0.6.4" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 546 | 547 | [[package]] 548 | name = "redox_users" 549 | version = "0.4.6" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 552 | dependencies = [ 553 | "getrandom", 554 | "libredox", 555 | "thiserror", 556 | ] 557 | 558 | [[package]] 559 | name = "rustix" 560 | version = "0.38.44" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 563 | dependencies = [ 564 | "bitflags 2.9.1", 565 | "errno", 566 | "libc", 567 | "linux-raw-sys", 568 | "windows-sys 0.59.0", 569 | ] 570 | 571 | [[package]] 572 | name = "rustversion" 573 | version = "1.0.20" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 576 | 577 | [[package]] 578 | name = "rustyline" 579 | version = "12.0.0" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" 582 | dependencies = [ 583 | "bitflags 2.9.1", 584 | "cfg-if", 585 | "clipboard-win", 586 | "fd-lock", 587 | "home", 588 | "libc", 589 | "log", 590 | "memchr", 591 | "nix", 592 | "radix_trie", 593 | "scopeguard", 594 | "unicode-segmentation", 595 | "unicode-width", 596 | "utf8parse", 597 | "winapi", 598 | ] 599 | 600 | [[package]] 601 | name = "ryu" 602 | version = "1.0.20" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 605 | 606 | [[package]] 607 | name = "scopeguard" 608 | version = "1.2.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 611 | 612 | [[package]] 613 | name = "serde" 614 | version = "1.0.219" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 617 | dependencies = [ 618 | "serde_derive", 619 | ] 620 | 621 | [[package]] 622 | name = "serde_derive" 623 | version = "1.0.219" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 626 | dependencies = [ 627 | "proc-macro2", 628 | "quote", 629 | "syn", 630 | ] 631 | 632 | [[package]] 633 | name = "serde_json" 634 | version = "1.0.140" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 637 | dependencies = [ 638 | "itoa", 639 | "memchr", 640 | "ryu", 641 | "serde", 642 | ] 643 | 644 | [[package]] 645 | name = "shlex" 646 | version = "1.3.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 649 | 650 | [[package]] 651 | name = "sigi" 652 | version = "3.7.2" 653 | dependencies = [ 654 | "chrono", 655 | "clap", 656 | "clearscreen", 657 | "directories", 658 | "json", 659 | "rustyline", 660 | "serde", 661 | "serde_json", 662 | ] 663 | 664 | [[package]] 665 | name = "siphasher" 666 | version = "1.0.1" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 669 | 670 | [[package]] 671 | name = "smallvec" 672 | version = "1.15.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 675 | 676 | [[package]] 677 | name = "str-buf" 678 | version = "1.0.6" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" 681 | 682 | [[package]] 683 | name = "strsim" 684 | version = "0.11.1" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 687 | 688 | [[package]] 689 | name = "syn" 690 | version = "2.0.101" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 693 | dependencies = [ 694 | "proc-macro2", 695 | "quote", 696 | "unicode-ident", 697 | ] 698 | 699 | [[package]] 700 | name = "terminfo" 701 | version = "0.8.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" 704 | dependencies = [ 705 | "dirs", 706 | "fnv", 707 | "nom", 708 | "phf", 709 | "phf_codegen", 710 | ] 711 | 712 | [[package]] 713 | name = "thiserror" 714 | version = "1.0.69" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 717 | dependencies = [ 718 | "thiserror-impl", 719 | ] 720 | 721 | [[package]] 722 | name = "thiserror-impl" 723 | version = "1.0.69" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 726 | dependencies = [ 727 | "proc-macro2", 728 | "quote", 729 | "syn", 730 | ] 731 | 732 | [[package]] 733 | name = "unicode-ident" 734 | version = "1.0.18" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 737 | 738 | [[package]] 739 | name = "unicode-segmentation" 740 | version = "1.12.0" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 743 | 744 | [[package]] 745 | name = "unicode-width" 746 | version = "0.1.14" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 749 | 750 | [[package]] 751 | name = "utf8parse" 752 | version = "0.2.2" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 755 | 756 | [[package]] 757 | name = "wasi" 758 | version = "0.11.0+wasi-snapshot-preview1" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 761 | 762 | [[package]] 763 | name = "wasm-bindgen" 764 | version = "0.2.100" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 767 | dependencies = [ 768 | "cfg-if", 769 | "once_cell", 770 | "rustversion", 771 | "wasm-bindgen-macro", 772 | ] 773 | 774 | [[package]] 775 | name = "wasm-bindgen-backend" 776 | version = "0.2.100" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 779 | dependencies = [ 780 | "bumpalo", 781 | "log", 782 | "proc-macro2", 783 | "quote", 784 | "syn", 785 | "wasm-bindgen-shared", 786 | ] 787 | 788 | [[package]] 789 | name = "wasm-bindgen-macro" 790 | version = "0.2.100" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 793 | dependencies = [ 794 | "quote", 795 | "wasm-bindgen-macro-support", 796 | ] 797 | 798 | [[package]] 799 | name = "wasm-bindgen-macro-support" 800 | version = "0.2.100" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 803 | dependencies = [ 804 | "proc-macro2", 805 | "quote", 806 | "syn", 807 | "wasm-bindgen-backend", 808 | "wasm-bindgen-shared", 809 | ] 810 | 811 | [[package]] 812 | name = "wasm-bindgen-shared" 813 | version = "0.2.100" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 816 | dependencies = [ 817 | "unicode-ident", 818 | ] 819 | 820 | [[package]] 821 | name = "which" 822 | version = "4.4.2" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 825 | dependencies = [ 826 | "either", 827 | "home", 828 | "once_cell", 829 | "rustix", 830 | ] 831 | 832 | [[package]] 833 | name = "winapi" 834 | version = "0.3.9" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 837 | dependencies = [ 838 | "winapi-i686-pc-windows-gnu", 839 | "winapi-x86_64-pc-windows-gnu", 840 | ] 841 | 842 | [[package]] 843 | name = "winapi-i686-pc-windows-gnu" 844 | version = "0.4.0" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 847 | 848 | [[package]] 849 | name = "winapi-x86_64-pc-windows-gnu" 850 | version = "0.4.0" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 853 | 854 | [[package]] 855 | name = "windows-core" 856 | version = "0.61.1" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" 859 | dependencies = [ 860 | "windows-implement", 861 | "windows-interface", 862 | "windows-link", 863 | "windows-result", 864 | "windows-strings", 865 | ] 866 | 867 | [[package]] 868 | name = "windows-implement" 869 | version = "0.60.0" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 872 | dependencies = [ 873 | "proc-macro2", 874 | "quote", 875 | "syn", 876 | ] 877 | 878 | [[package]] 879 | name = "windows-interface" 880 | version = "0.59.1" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 883 | dependencies = [ 884 | "proc-macro2", 885 | "quote", 886 | "syn", 887 | ] 888 | 889 | [[package]] 890 | name = "windows-link" 891 | version = "0.1.1" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 894 | 895 | [[package]] 896 | name = "windows-result" 897 | version = "0.3.3" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" 900 | dependencies = [ 901 | "windows-link", 902 | ] 903 | 904 | [[package]] 905 | name = "windows-strings" 906 | version = "0.4.1" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" 909 | dependencies = [ 910 | "windows-link", 911 | ] 912 | 913 | [[package]] 914 | name = "windows-sys" 915 | version = "0.48.0" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 918 | dependencies = [ 919 | "windows-targets 0.48.5", 920 | ] 921 | 922 | [[package]] 923 | name = "windows-sys" 924 | version = "0.59.0" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 927 | dependencies = [ 928 | "windows-targets 0.52.6", 929 | ] 930 | 931 | [[package]] 932 | name = "windows-targets" 933 | version = "0.48.5" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 936 | dependencies = [ 937 | "windows_aarch64_gnullvm 0.48.5", 938 | "windows_aarch64_msvc 0.48.5", 939 | "windows_i686_gnu 0.48.5", 940 | "windows_i686_msvc 0.48.5", 941 | "windows_x86_64_gnu 0.48.5", 942 | "windows_x86_64_gnullvm 0.48.5", 943 | "windows_x86_64_msvc 0.48.5", 944 | ] 945 | 946 | [[package]] 947 | name = "windows-targets" 948 | version = "0.52.6" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 951 | dependencies = [ 952 | "windows_aarch64_gnullvm 0.52.6", 953 | "windows_aarch64_msvc 0.52.6", 954 | "windows_i686_gnu 0.52.6", 955 | "windows_i686_gnullvm", 956 | "windows_i686_msvc 0.52.6", 957 | "windows_x86_64_gnu 0.52.6", 958 | "windows_x86_64_gnullvm 0.52.6", 959 | "windows_x86_64_msvc 0.52.6", 960 | ] 961 | 962 | [[package]] 963 | name = "windows_aarch64_gnullvm" 964 | version = "0.48.5" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 967 | 968 | [[package]] 969 | name = "windows_aarch64_gnullvm" 970 | version = "0.52.6" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 973 | 974 | [[package]] 975 | name = "windows_aarch64_msvc" 976 | version = "0.48.5" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 979 | 980 | [[package]] 981 | name = "windows_aarch64_msvc" 982 | version = "0.52.6" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 985 | 986 | [[package]] 987 | name = "windows_i686_gnu" 988 | version = "0.48.5" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 991 | 992 | [[package]] 993 | name = "windows_i686_gnu" 994 | version = "0.52.6" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 997 | 998 | [[package]] 999 | name = "windows_i686_gnullvm" 1000 | version = "0.52.6" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1003 | 1004 | [[package]] 1005 | name = "windows_i686_msvc" 1006 | version = "0.48.5" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1009 | 1010 | [[package]] 1011 | name = "windows_i686_msvc" 1012 | version = "0.52.6" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1015 | 1016 | [[package]] 1017 | name = "windows_x86_64_gnu" 1018 | version = "0.48.5" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1021 | 1022 | [[package]] 1023 | name = "windows_x86_64_gnu" 1024 | version = "0.52.6" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1027 | 1028 | [[package]] 1029 | name = "windows_x86_64_gnullvm" 1030 | version = "0.48.5" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1033 | 1034 | [[package]] 1035 | name = "windows_x86_64_gnullvm" 1036 | version = "0.52.6" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1039 | 1040 | [[package]] 1041 | name = "windows_x86_64_msvc" 1042 | version = "0.48.5" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1045 | 1046 | [[package]] 1047 | name = "windows_x86_64_msvc" 1048 | version = "0.52.6" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1051 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sigi" 3 | version = "3.7.2" 4 | authors = ["Justin \"Boonie Pepper\" Hill "] 5 | edition = "2021" 6 | license = "GPL-2.0-only" 7 | description = "An organizing tool for terminal lovers who hate organizing" 8 | readme = "README.md" 9 | homepage = "https://github.com/sigi-cli/sigi" 10 | repository = "https://github.com/sigi-cli/sigi" 11 | documentation = "https://docs.rs/sigi" 12 | keywords = ["organization", "planning", "stack", "todo", "cli"] 13 | categories = ["command-line-interface"] 14 | 15 | [badges] 16 | maintenance = { status = "actively-developed" } 17 | 18 | [profile.release] 19 | codegen-units = 1 20 | lto = true 21 | opt-level = 'z' 22 | 23 | [dependencies] 24 | chrono = { version = "0.4", features = [ "serde", "unstable-locales" ] } 25 | clap = { version = "4.4", features = [ "derive" ] } 26 | clearscreen = "2.0" 27 | directories = "5.0" 28 | json = "0.12.4" 29 | rustyline = "12.0" 30 | serde = { version = "1.0", features = [ "derive" ] } 31 | serde_json = "1.0" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # Similar projects 2 | 3 | If sigi doesn't do quite what you want, check out these similar projects. Sigi 4 | was created before I found these, but some inspiration may be gleaned from them 5 | for improvement. Most in this list predate sigi by several years. 6 | 7 | ## Similar CLIs 8 | 9 | - [devtodo](https://swapoff.org/devtodo.html) - A hierarchical command-line task manager 10 | - [dstask](https://github.com/naggie/dstask) - Single binary terminal-based TODO manager with git-based sync + markdown notes per task 11 | - [geek-life](https://github.com/ajaxray/geek-life) - The CLI To-Do List / Task Manager for Geeks 12 | - [grit](https://github.com/climech/grit) - Multitree-based task manager 13 | - [node-todo-cli](https://www.npmjs.com/package/node-todo-cli) - A command line program that manages todo tasks 14 | - [py-todo-cli](https://github.com/Mantaseus/Todo-CLI) - A simple command line Todo program written in Python 15 | - [taskell](https://taskell.app) - Command-line Kanban board/task management 16 | - [taskwarrior](https://taskwarrior.org) - Taskwarrior is Free and Open Source Software that manages your TODO list from the command line 17 | - [tax](https://github.com/netgusto/tax) - CLI task list manager 18 | - [todo cli](https://gitlab.com/bigfiga99/todo-cli) - Todo CLI is a simple program that uses a sqlite3 database to keep track of your tasks 19 | - [todo.txt](http://todotxt.org) - Future-proof task tracking in a file you control 20 | - [todo.txt cli](https://github.com/todotxt/todo.txt-cli) 21 | - [ultralist](https://ultralist.io) - Command-line task management for tech folks 22 | - [yokadi](https://yokadi.github.io) - Yokadi is a command line oriented, sqlite powered, todo-list 23 | 24 | ### Similar CLI Definitions 25 | 26 | - gophercises #7: [task](https://github.com/gophercises/task) - TODO CLI definition. (Defines a CLI) 27 | - [pushpop](https://github.com/secretGeek/pushpop) - "Mental stack manager" definition. (Defines both a GUI and CLI) 28 | - See also implementations in [sh](https://paste.sr.ht/~erazemkokot/c6aeb2a7bc25049d08825b3cc7aea63b5cf72a08), [power shell](https://github.com/kberridge/psushpop/blob/master/psushpop.psm1) 29 | 30 | ## Similar non-CLI apps 31 | 32 | - [GTG (Getting Things GNOME!)](https://wiki.gnome.org/Apps/GTG) 33 | - [KTimeTracker](https://userbase.kde.org/KTimeTracker) 34 | - But really... Just too many to list? TODO apps are kind of the canonical JavaScript "first big project." They're also ubiquitous in mobile app stores. 35 | 36 | ## Similar Databases 37 | 38 | - [piladb](https://github.com/fern4lvarez/piladb) - Stack-based database. (A working REST API and Database) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://sigi-cli.org) 2 | 3 | [![crates.io version](https://img.shields.io/crates/v/sigi)](https://crates.io/crates/sigi) 4 | [![crates.io downloads](https://img.shields.io/crates/d/sigi?label=crates.io%20downloads)](https://crates.io/crates/sigi) 5 | [![docs.rs docs](https://docs.rs/mio/badge.svg)](https://docs.rs/sigi) 6 | [![discord badge](https://img.shields.io/discord/1141777454164365382?logo=discord)](https://discord.gg/Yehv682GJ4) 7 | 8 | # Sigi CLI 9 | 10 | `sigi` is an organizing tool for terminal lovers who hate organizing 11 | 12 | Use `sigi` as extra memory. Use it to toss your tasks, groceries, or the next 13 | board games you want to play onto a stack. Shell aliases are encouraged to 14 | organize your various stacks. 15 | 16 | --- 17 | 18 | ```console 19 | $ sigi -h 20 | An organizing tool for terminal lovers who hate organizing 21 | 22 | Usage: sigi [OPTIONS] [COMMAND] 23 | 24 | Commands: 25 | interactive Run in an interactive mode [aliases: i] 26 | - Read input lines from standard input. Same commands as interactive mode, but only prints for printing commands. Intended for use in unix pipes 27 | complete Move the current item to "_history" and mark as completed [aliases: done, finish, fulfill] 28 | count Print the total number of items in the stack [aliases: size, length] 29 | delete Move the current item to "_history" and mark as deleted [aliases: pop, remove, cancel, drop] 30 | delete-all Move all items to "_history" and mark as deleted [aliases: purge, pop-all, remove-all, cancel-all, drop-all] 31 | edit Edit the content of an item. Other metadata like creation date is left unchanged 32 | head Print the first N items (default is 10) [aliases: top, first] 33 | is-empty Print "true" if stack has zero items, or print "false" (and exit with a nonzero exit code) if the stack does have items [aliases: empty] 34 | list Print all items [aliases: ls, snoop, all] 35 | list-stacks Print all stacks [aliases: stacks] 36 | move Move current item to another stack 37 | move-all Move all items to another stack 38 | next Cycle to the next item; the current item becomes last [aliases: later, cycle, bury] 39 | peek Print the first item. This is the default CLI behavior when no command is given [aliases: show] 40 | pick Move items to the top of stack by their number 41 | push Create a new item [aliases: create, add, do, start, new] 42 | rot Rotate the three most-current items [aliases: rotate] 43 | swap Swap the two most-current items 44 | tail Print the last N items (default is 10) [aliases: bottom, last] 45 | help Print this message or the help of the given subcommand(s) 46 | 47 | Options: 48 | -q, --quiet Omit any leading labels or symbols. Recommended for use in shell scripts 49 | -s, --silent Omit any output at all 50 | -v, --verbose Print more information, like when an item was created [aliases: noisy] 51 | -f, --format Use a programmatic format. Options include [csv, json, json-compact, tsv]. Not compatible with quiet/silent/verbose [possible values: csv, json, json-compact, tsv] 52 | -t, --stack Manage items in a specific stack [aliases: topic, about, namespace] 53 | -d, --data-store (Advanced) Manage sigi stacks in a specific directory. The default is either the value of a SIGI_HOME environment variable or your OS-specific home directory [aliases: dir, directory, store] 54 | -h, --help Print help (see more with '--help') 55 | -V, --version Print version 56 | 57 | INTERACTIVE MODE: 58 | 59 | Use subcommands in interactive mode directly. No OPTIONS (flags) are understood in interactive mode. The ; character can be used to separate commands. 60 | 61 | The following additional commands are available: 62 | ? Show the short version of "help" 63 | clear Clear the terminal screen 64 | use Change to the specified stack [aliases: stack] 65 | exit Quit interactive mode [aliases: quit, q] 66 | ``` 67 | 68 | # Examples 69 | 70 | ## `sigi` as a to-do list 71 | 72 | `sigi` can understand `do` (create a task) and `done` (complete a task). 73 | 74 | ```console 75 | $ alias todo='sigi --stack todo' 76 | 77 | $ todo do Write some code 78 | Creating: Write some code 79 | 80 | $ todo do Get a drink 81 | Creating: Get a drink 82 | 83 | $ todo do Take a nap 84 | Creating: Take a nap 85 | 86 | $ todo list 87 | Now: Take a nap 88 | 1: Get a drink 89 | 2: Write some code 90 | 91 | $ sleep 20m 92 | 93 | $ todo done 94 | Completed: Take a nap 95 | ``` 96 | 97 | It's best to use `sigi` behind a few aliases with unique "stacks". You should 98 | save these aliases in your `~/.bashrc` or `~/.zshrc` or whatever your shell has 99 | for configuration. `sigi` accepts a `--stack` option, and you can have as many 100 | stacks as you can think of names. 101 | 102 | Forgot what to do next? 103 | 104 | ```console 105 | $ todo 106 | Now: Get a drink 107 | ``` 108 | 109 | Not going to do it? 110 | 111 | ```console 112 | $ todo delete 113 | Deleted: Get a drink 114 | ``` 115 | 116 | ## `sigi` as a save-anything list 117 | 118 | Extending the alias idea, you can use `sigi` to store anything you want to 119 | remember later. 120 | 121 | ```console 122 | $ alias watch-later='sigi --stack watch-later' 123 | 124 | $ watch-later add One Punch Man 125 | Creating: One Punch Man 126 | ``` 127 | 128 | ```console 129 | $ alias story-ideas='sigi --stack=story-ideas' 130 | 131 | $ story-ideas add Alien race lives backwards through time. 132 | Creating: Alien race lives backwards through time. 133 | ``` 134 | 135 | ## `sigi` remote via ssh 136 | 137 | If you have a host you can access remotely, using a tool like 138 | [OpenSSH](https://www.openssh.com), you can also use sigi across machines. 139 | Consider using an alias like this: 140 | 141 | ```console 142 | $ alias home-todo='ssh -qt user@host.or.ip sigi --stack=home-todo' 143 | ``` 144 | 145 | > Protip: If you do a bunch of machine hopping via SSH, consider adding host 146 | aliases in [`$HOME/.ssh/config`](https://man.openbsd.org/ssh_config.5). I set 147 | these up something like this: 148 | 149 | > ```ssh-config 150 | > Host hq 151 | > User boonieppper 152 | > HostName 192.168.x.x 153 | > IdentityFile ~/.ssh/etc 154 | > ``` 155 | > which allows for just running `ssh hq`, for example. 156 | 157 | ## `sigi` as a local stack-based database 158 | 159 | `sigi` understands the programmer-familiar `push` and `pop` idioms. It can be 160 | used for simple, persistent, small-scale stack use-cases. 161 | 162 | Using the `--quiet` (or `-q`) flag is recommended for shell scripts, as it 163 | leaves out any leading labels or symbols. If used with a pipe, it's recommended 164 | to use the `-` subcommand to read from standard input and only print if the 165 | action requested is a printing action (like `list`). 166 | 167 | `sigi` is pretty fast: sub-millisecond for basic use cases. That said, it is 168 | not intended to handle large amounts of data, or concurrent throughput. For 169 | something beefier with stack semantics, check out Redis. 170 | 171 | # Installing 172 | 173 | [![Packaging status](https://repology.org/badge/vertical-allrepos/sigi.svg)](https://repology.org/project/sigi/versions) 174 | 175 | If your packaging system doesn't have it yet, the best way to install `sigi` is 176 | through the Rust language package manager, `cargo`: 177 | 178 | ```console 179 | $ cargo install sigi 180 | ``` 181 | 182 | Instructions on installing `cargo` can be found here: 183 | 184 | - https://doc.rust-lang.org/cargo/getting-started/installation.html 185 | 186 | Please package it up for your Linux/BSD/etc distribution. 187 | 188 | # Contributing and support 189 | 190 | Please [open an issue](https://github.com/sigi-cli/sigi/issues) if you see 191 | bugs or have ideas! 192 | 193 | I'm looking for people to use [the `sigi` wiki](https://github.com/sigi-cli/sigi/wiki) 194 | to share their tips, tricks, and examples. 195 | 196 | Thanks for checking it out! 197 | -------------------------------------------------------------------------------- /sigi.1: -------------------------------------------------------------------------------- 1 | .TH sigi 1 "May 16, 2025" "version 3.7.2" "USER COMMANDS" 2 | .\" 3 | .SH NAME 4 | sigi \- An organizing tool for terminal lovers who hate organizing 5 | .\" 6 | .SH SYNOPSIS 7 | .B sigi 8 | [FLAGS] [OPTIONS] [SUBCOMMAND] 9 | .\" 10 | .\" ================================ 11 | .\" 12 | .SH DESCRIPTION 13 | Use sigi as extra memory. Use it to organize your tasks, groceries, or the next 14 | board games you want to play... as stacks! Shell aliases are strongly 15 | encouraged to organize your various stacks. 16 | .PP 17 | .I Sigi 18 | is the Chamorro word for 19 | .I continue. 20 | I hope this will help you to plan more, forget less, get things done, and relax. 21 | .\" 22 | .\" ================================ 23 | .\" 24 | .SH FLAGS 25 | .TP 26 | \-h, \-\-help 27 | Prints help information. 28 | .TP 29 | \-q, \-\-quiet 30 | Omit any leading labels or symbols. Recommended for use in shell scripts. 31 | .TP 32 | \-s, \-\-silent 33 | Omit any output at all. 34 | .TP 35 | \-V, \-\-version 36 | Prints version information. 37 | .TP 38 | \-v, \-\-verbose [Aliases: \-\-noisy] 39 | Prints more information, like when an item was created. 40 | .\" 41 | .\" ================================ 42 | .\" 43 | .SH OPTIONS 44 | .TP 45 | \-f, \-\-format 46 | Use a programmatic FORMAT. Options include: [csv, json, json-compact, tsv] 47 | .TP 48 | \-t, \-\-stack [Or: \-\-topic, \-\-about, \-\-namespace ] 49 | Manage items in a specific STACK. If no STACK is provided, it will use "sigi" 50 | by default. It's recommended to use shell aliases to access your stacks. (See 51 | .B EXAMPLES 52 | below.) 53 | .TP 54 | \-d, \-\-data\-store 55 | (Advanced) Manage sigi stacks in a specific directory. The default is either 56 | the value of a SIGI_HOME environment variable or your OS-specific home 57 | directory [aliases: dir, directory, store] 58 | .\" 59 | .\" ================================ 60 | .\" 61 | .SH SUBCOMMANDS 62 | .TP 63 | - 64 | Read input lines from standard input. Same commands as interactive mode, but 65 | only prints for printing commands. Intended for use in unix pipes 66 | .TP 67 | complete 68 | Move the current item to "_history" and mark as completed [aliases: done, finish, fulfill] 69 | .TP 70 | count 71 | Print the total number of items in the stack [aliases: size, length] 72 | .TP 73 | delete 74 | Move the current item to "_history" and mark as deleted. [aliases: pop, remove, cancel, drop] 75 | .TP 76 | delete-all 77 | Move all items to "_history" and mark as deleted [aliases: purge, pop-all, remove-all, cancel-all, drop-all] 78 | .TP 79 | edit 80 | Edit the content of an item. Other metadata like creation date is left unchanged 81 | .TP 82 | head N 83 | Print the first N items [aliases: top, first] 84 | .TP 85 | help 86 | Prints a help message or the help of the given subcommand(s) 87 | .TP 88 | interactive 89 | Run in an interactive mode [aliases: i] 90 | .TP 91 | is-empty 92 | Prints "true" if stack has zero items, or prints "false" (fails with a nonzero exit code) if the stack does have items [aliases: empty] 93 | .TP 94 | list 95 | Print all items [aliases: ls, snoop, show, all] 96 | .TP 97 | list-stacks 98 | Print all stacks [aliases: stacks] 99 | .TP 100 | move 101 | Move current item to another stack 102 | .TP 103 | move-all 104 | Move all items to another stack 105 | .TP 106 | next 107 | Cycle to the next item; the current item becomes last [aliases: later, cycle, bury] 108 | .TP 109 | peek 110 | Print the first item. (This is the default behavior when no command is given) [aliases: show] 111 | .TP 112 | pick 113 | Move items to the top of stack by their number 114 | .TP 115 | push 116 | Create a new item [aliases: create, add, do, start, new] 117 | .TP 118 | rot 119 | Rotate the three most-current items [aliases: rotate] 120 | .TP 121 | swap 122 | Swap the two most-current items 123 | .TP 124 | tail 125 | Print the last N items [aliases: bottom, last] 126 | .\" 127 | .\" ================================ 128 | .\" Note to self: preconv can do utf8 -> troff escapes. 129 | .\" 130 | .SH INTERACTIVE MODE 131 | Use subcommands in interactive mode directly. For example: 132 | .RS 133 | .EX 134 | \t\[u1F334] \[u25B6] push a new thing 135 | Created: a new thing 136 | \[u1F334] \[u25B6] peek 137 | Now: a new thing 138 | \[u1F334] \[u25B6] delete 139 | Deleted: a new thing 140 | Now: nothing 141 | \[u1F334] \[u25B6] exit 142 | exit: Buen bi\[u00E5]he! 143 | .EE 144 | .RE 145 | .PP 146 | No OPTIONS (flags) of subcommands are understood in interactive mode. 147 | .PP 148 | The ; character can be used to separate commands. 149 | .PP 150 | The following additional commands are available: 151 | .RS 152 | .TP 153 | ? 154 | Show the short version of \"help\" 155 | .TP 156 | clear 157 | Clear the terminal screen 158 | .TP 159 | use 160 | Change to the specified stack [aliases: stack] 161 | .TP 162 | exit 163 | Quit interactive mode [aliases: quit, q] 164 | .RE 165 | .\" 166 | .\" ================================ 167 | .\" 168 | .SH EXAMPLES, CONTRIBUTING, AND SUPPORT 169 | See: https://github.com/sigi-cli/sigi 170 | .\" 171 | .SH AUTHOR 172 | J.R. Hill https://so.dang.cool 173 | -------------------------------------------------------------------------------- /src/bin/sigi.rs: -------------------------------------------------------------------------------- 1 | /// Run the CLI 2 | fn main() { 3 | sigi::cli::run(); 4 | } 5 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::data::{DataFormat, DataStore, WorkingDir}; 2 | use crate::effects::StackEffect; 3 | use crate::output::{NoiseLevel, OutputFormat}; 4 | use clap::{Args, Parser, Subcommand, ValueEnum}; 5 | use std::str::FromStr; 6 | use std::{error, fmt}; 7 | 8 | mod interact; 9 | use interact::*; 10 | 11 | /// The current version of the CLI. (As defined in Cargo.toml) 12 | pub const SIGI_VERSION: &str = std::env!("CARGO_PKG_VERSION"); 13 | 14 | const DEFAULT_STACK_NAME: &str = "sigi"; 15 | const DEFAULT_FORMAT: OutputFormat = OutputFormat::Human(NoiseLevel::Normal); 16 | const DEFAULT_DATA_STORE: DataStore = DataStore { 17 | working_dir: WorkingDir::HomeDir, 18 | data_format: DataFormat::SigiJson, 19 | }; 20 | const DEFAULT_SHORT_LIST_LIMIT: usize = 10; 21 | 22 | // === Glossary === 23 | const COMPLETE_TERMS: [&str; 4] = ["complete", "done", "finish", "fulfill"]; 24 | const COUNT_TERMS: [&str; 3] = ["count", "size", "length"]; 25 | const DELETE_TERMS: [&str; 5] = ["delete", "pop", "remove", "cancel", "drop"]; 26 | const DELETE_ALL_TERMS: [&str; 6] = [ 27 | "delete-all", 28 | "purge", 29 | "pop-all", 30 | "remove-all", 31 | "cancel-all", 32 | "drop-all", 33 | ]; 34 | const EDIT_TERMS: [&str; 1] = ["edit"]; 35 | const HEAD_TERMS: [&str; 3] = ["head", "top", "first"]; 36 | const IS_EMPTY_TERMS: [&str; 2] = ["is-empty", "empty"]; 37 | const LIST_TERMS: [&str; 4] = ["list", "ls", "snoop", "all"]; 38 | const LIST_STACKS_TERMS: [&str; 2] = ["list-stacks", "stacks"]; 39 | const MOVE_TERMS: [&str; 1] = ["move"]; 40 | const MOVE_ALL_TERMS: [&str; 1] = ["move-all"]; 41 | const NEXT_TERMS: [&str; 4] = ["next", "later", "cycle", "bury"]; 42 | const PEEK_TERMS: [&str; 2] = ["peek", "show"]; 43 | const PICK_TERMS: [&str; 1] = ["pick"]; 44 | const PUSH_TERMS: [&str; 6] = ["push", "create", "add", "do", "start", "new"]; 45 | const ROT_TERMS: [&str; 2] = ["rot", "rotate"]; 46 | const SWAP_TERMS: [&str; 1] = ["swap"]; 47 | const TAIL_TERMS: [&str; 3] = ["tail", "bottom", "last"]; 48 | // === /glossary === 49 | 50 | pub fn run() { 51 | let args = Cli::parse(); 52 | 53 | let stack = args.stack.unwrap_or_else(|| DEFAULT_STACK_NAME.into()); 54 | let store = args 55 | .data_store 56 | .map(|dir| DataStore { 57 | working_dir: WorkingDir::Dir(dir), 58 | data_format: DataFormat::SigiJson, 59 | }) 60 | .unwrap_or(DEFAULT_DATA_STORE); 61 | 62 | match args.mode { 63 | None => { 64 | let output = args.fc.into_output_format().unwrap_or(DEFAULT_FORMAT); 65 | let peek = StackEffect::Peek { stack }; 66 | peek.run(&store, &output); 67 | } 68 | Some(Mode::Command(command)) => { 69 | let (effect, effect_fc) = command.into_effect_and_fc(stack); 70 | let output = args.fc.into_fallback_for(effect_fc); 71 | effect.run(&store, &output); 72 | } 73 | Some(Mode::Interactive { fc }) => { 74 | let output = args.fc.into_fallback_for(fc); 75 | interact(stack, store, output); 76 | } 77 | Some(Mode::ReadStdin) => interact(stack, store, OutputFormat::TerseText), 78 | }; 79 | } 80 | 81 | #[derive(Parser)] 82 | #[command(name = "sigi", version = SIGI_VERSION, after_help = INTERACT_INSTRUCTIONS, after_long_help = INTERACT_LONG_INSTRUCTIONS)] 83 | /// An organizing tool for terminal lovers who hate organizing 84 | struct Cli { 85 | #[command(flatten)] 86 | fc: FormatConfig, 87 | 88 | /// Manage items in a specific stack 89 | #[arg(short='t', long, visible_aliases = &["topic", "about", "namespace"])] 90 | stack: Option, 91 | 92 | /// (Advanced) Manage sigi stacks in a specific directory. The default is either the value of a SIGI_HOME environment variable or your OS-specific home directory 93 | #[arg(short = 'd', long, visible_aliases = &["dir", "directory", "store"])] 94 | data_store: Option, 95 | 96 | #[command(subcommand)] 97 | mode: Option, 98 | } 99 | 100 | #[derive(Subcommand)] 101 | enum Mode { 102 | /// Run in an interactive mode 103 | #[command(visible_alias = "i")] 104 | Interactive { 105 | #[command(flatten)] 106 | fc: FormatConfig, 107 | }, 108 | 109 | /// Read input lines from standard input. Same commands as interactive 110 | /// mode, but only prints for printing commands. Intended for use in unix 111 | /// pipes 112 | #[command(name = "-")] 113 | ReadStdin, 114 | 115 | #[command(flatten)] 116 | Command(Command), 117 | } 118 | 119 | #[derive(Subcommand)] 120 | enum Command { 121 | /// Move the current item to "_history" and mark as completed 122 | #[command(visible_aliases = &COMPLETE_TERMS[1..])] 123 | Complete { 124 | /// The number of the item to complete. Default is the most recent item (0 index) 125 | n: Option, 126 | 127 | #[command(flatten)] 128 | fc: FormatConfig, 129 | }, 130 | 131 | /// Print the total number of items in the stack 132 | #[command(visible_aliases = &COUNT_TERMS[1..])] 133 | Count { 134 | #[command(flatten)] 135 | fc: FormatConfig, 136 | }, 137 | 138 | /// Move the current item to "_history" and mark as deleted 139 | #[command(visible_aliases = &DELETE_TERMS[1..])] 140 | Delete { 141 | /// The number of the item to delete. Default is the most recent item (0 index) 142 | n: Option, 143 | 144 | #[command(flatten)] 145 | fc: FormatConfig, 146 | }, 147 | 148 | /// Move all items to "_history" and mark as deleted 149 | #[command(visible_aliases = &DELETE_ALL_TERMS[1..])] 150 | DeleteAll { 151 | #[command(flatten)] 152 | fc: FormatConfig, 153 | }, 154 | 155 | /// Edit the content of an item. Other metadata like creation date is left unchanged. 156 | #[command(visible_aliases = &EDIT_TERMS[1..])] 157 | Edit { 158 | /// The editor to execute. If unspecified, the editor launched will be the value of 159 | /// VISUAL, EDITOR, or if both env variables are unset, nano. 160 | #[arg(short, long)] 161 | editor: Option, 162 | 163 | /// The number of the item to edit. Default is the most recent item (0 index) 164 | n: Option, 165 | 166 | #[command(flatten)] 167 | fc: FormatConfig, 168 | }, 169 | 170 | /// Print the first N items (default is 10) 171 | #[command(visible_aliases = &HEAD_TERMS[1..])] 172 | Head { 173 | /// The number of items to display 174 | n: Option, 175 | 176 | #[command(flatten)] 177 | fc: FormatConfig, 178 | }, 179 | 180 | /// Print "true" if stack has zero items, or print "false" (and exit with a 181 | /// nonzero exit code) if the stack does have items 182 | #[command(visible_aliases = &IS_EMPTY_TERMS[1..])] 183 | IsEmpty { 184 | #[command(flatten)] 185 | fc: FormatConfig, 186 | }, 187 | 188 | /// Print all items 189 | #[command(visible_aliases = &LIST_TERMS[1..])] 190 | List { 191 | #[command(flatten)] 192 | fc: FormatConfig, 193 | }, 194 | 195 | /// Print all stacks 196 | #[command(visible_aliases = &LIST_STACKS_TERMS[1..])] 197 | ListStacks { 198 | #[command(flatten)] 199 | fc: FormatConfig, 200 | }, 201 | 202 | /// Move current item to another stack 203 | #[command(arg_required_else_help = true, visible_aliases = &MOVE_TERMS[1..])] 204 | Move { 205 | #[arg(name = "destination")] 206 | /// The stack that will get the source stack's current item 207 | dest: String, 208 | 209 | #[command(flatten)] 210 | fc: FormatConfig, 211 | }, 212 | 213 | /// Move all items to another stack 214 | #[command(arg_required_else_help = true, visible_aliases = &MOVE_ALL_TERMS[1..])] 215 | MoveAll { 216 | #[arg(name = "destination")] 217 | /// The stack that will get all the source stack's items 218 | dest: String, 219 | 220 | #[command(flatten)] 221 | fc: FormatConfig, 222 | }, 223 | 224 | /// Cycle to the next item; the current item becomes last 225 | #[command(visible_aliases = &NEXT_TERMS[1..])] 226 | Next { 227 | #[command(flatten)] 228 | fc: FormatConfig, 229 | }, 230 | 231 | /// Print the first item. This is the default CLI behavior when no command is given 232 | #[command(visible_aliases = &PEEK_TERMS[1..])] 233 | Peek { 234 | #[command(flatten)] 235 | fc: FormatConfig, 236 | }, 237 | 238 | /// Move items to the top of stack by their number 239 | #[command(visible_aliases = &PICK_TERMS[1..])] 240 | Pick { 241 | ns: Vec, 242 | 243 | #[command(flatten)] 244 | fc: FormatConfig, 245 | }, 246 | 247 | /// Create a new item 248 | #[command(visible_aliases = &PUSH_TERMS[1..])] 249 | Push { 250 | // The content to add as an item. Multiple arguments will be interpreted as a single string 251 | content: Vec, 252 | 253 | #[command(flatten)] 254 | fc: FormatConfig, 255 | }, 256 | 257 | /// Rotate the three most-current items 258 | #[command(visible_aliases = &ROT_TERMS[1..])] 259 | Rot { 260 | #[command(flatten)] 261 | fc: FormatConfig, 262 | }, 263 | 264 | /// Swap the two most-current items 265 | #[command(visible_aliases = &SWAP_TERMS[1..])] 266 | Swap { 267 | #[command(flatten)] 268 | fc: FormatConfig, 269 | }, 270 | 271 | /// Print the last N items (default is 10) 272 | #[command(visible_aliases = &TAIL_TERMS[1..])] 273 | Tail { 274 | /// The number of items to display 275 | n: Option, 276 | 277 | #[command(flatten)] 278 | fc: FormatConfig, 279 | }, 280 | } 281 | 282 | impl Command { 283 | fn into_effect_and_fc(self, stack: String) -> (StackEffect, FormatConfig) { 284 | use StackEffect::*; 285 | match self { 286 | Command::Complete { n, fc } => ( 287 | Complete { 288 | stack, 289 | index: n.unwrap_or(0), 290 | }, 291 | fc, 292 | ), 293 | Command::Count { fc } => (Count { stack }, fc), 294 | Command::Delete { n, fc } => ( 295 | Delete { 296 | stack, 297 | index: n.unwrap_or(0), 298 | }, 299 | fc, 300 | ), 301 | Command::DeleteAll { fc } => (DeleteAll { stack }, fc), 302 | Command::Edit { editor, n, fc } => ( 303 | Edit { 304 | stack, 305 | editor: resolve_editor(editor), 306 | index: n.unwrap_or(0), 307 | }, 308 | fc, 309 | ), 310 | Command::Head { n, fc } => { 311 | let n = n.unwrap_or(DEFAULT_SHORT_LIST_LIMIT); 312 | (Head { n, stack }, fc) 313 | } 314 | Command::IsEmpty { fc } => (IsEmpty { stack }, fc), 315 | Command::List { fc } => (ListAll { stack }, fc), 316 | Command::ListStacks { fc } => (ListStacks, fc), 317 | Command::Move { dest, fc } => (Move { stack, dest }, fc), 318 | Command::MoveAll { dest, fc } => (MoveAll { stack, dest }, fc), 319 | Command::Next { fc } => (Next { stack }, fc), 320 | Command::Peek { fc } => (Peek { stack }, fc), 321 | Command::Pick { ns, fc } => (Pick { stack, indices: ns }, fc), 322 | Command::Push { content, fc } => { 323 | let content = content.join(" "); 324 | (Push { stack, content }, fc) 325 | } 326 | Command::Rot { fc } => (Rot { stack }, fc), 327 | Command::Swap { fc } => (Swap { stack }, fc), 328 | Command::Tail { n, fc } => { 329 | let n = n.unwrap_or(DEFAULT_SHORT_LIST_LIMIT); 330 | (Tail { n, stack }, fc) 331 | } 332 | } 333 | } 334 | } 335 | 336 | pub fn resolve_editor(editor: Option) -> String { 337 | editor 338 | .or_else(|| std::env::var("VISUAL").ok()) 339 | .or_else(|| std::env::var("EDITOR").ok()) 340 | .unwrap_or("nano".into()) 341 | } 342 | 343 | #[derive(Args)] 344 | struct FormatConfig { 345 | #[arg(short, long)] 346 | /// Omit any leading labels or symbols. Recommended for use in shell scripts 347 | quiet: bool, 348 | 349 | #[arg(short, long)] 350 | /// Omit any output at all 351 | silent: bool, 352 | 353 | #[arg(short, long, visible_alias = "noisy")] 354 | /// Print more information, like when an item was created 355 | verbose: bool, 356 | 357 | #[arg(short, long)] 358 | /// Use a programmatic format. Options include [csv, json, json-compact, tsv]. Not compatible with quiet/silent/verbose. 359 | format: Option, 360 | } 361 | 362 | impl FormatConfig { 363 | fn into_output_format(self) -> Option { 364 | let FormatConfig { 365 | verbose, 366 | silent, 367 | quiet, 368 | format, 369 | } = self; 370 | 371 | use NoiseLevel::*; 372 | use OutputFormat::*; 373 | 374 | format 375 | .map(|format| match format { 376 | ProgrammaticFormat::Csv => Csv, 377 | ProgrammaticFormat::Json => Json, 378 | ProgrammaticFormat::JsonCompact => JsonCompact, 379 | ProgrammaticFormat::Tsv => Tsv, 380 | }) 381 | .or(if verbose { 382 | Some(Human(Verbose)) 383 | } else if silent { 384 | Some(Silent) 385 | } else if quiet { 386 | Some(Human(Quiet)) 387 | } else { 388 | None 389 | }) 390 | } 391 | 392 | fn into_fallback_for(self, fc: FormatConfig) -> OutputFormat { 393 | fc.into_output_format() 394 | .or_else(|| self.into_output_format()) 395 | .unwrap_or(DEFAULT_FORMAT) 396 | } 397 | } 398 | 399 | #[derive(ValueEnum, Clone)] 400 | enum ProgrammaticFormat { 401 | Csv, 402 | Json, 403 | JsonCompact, 404 | Tsv, 405 | } 406 | 407 | impl FromStr for ProgrammaticFormat { 408 | type Err = UnknownFormat; 409 | 410 | fn from_str(format: &str) -> Result { 411 | use ProgrammaticFormat::*; 412 | 413 | let format = format.to_ascii_lowercase(); 414 | 415 | match format.as_str() { 416 | "csv" => Ok(Csv), 417 | "json" => Ok(Json), 418 | "json-compact" => Ok(JsonCompact), 419 | "tsv" => Ok(Tsv), 420 | _ => Err(UnknownFormat { format }), 421 | } 422 | } 423 | } 424 | 425 | #[derive(Debug)] 426 | struct UnknownFormat { 427 | format: String, 428 | } 429 | 430 | impl error::Error for UnknownFormat {} 431 | 432 | impl fmt::Display for UnknownFormat { 433 | fn fmt(&self, out: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 434 | write!(out, "Unknown format: {}", self.format) 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /src/cli/interact.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::effects::StackEffect; 3 | use crate::output::OutputFormat; 4 | use clap::CommandFactory; 5 | use rustyline::error::ReadlineError; 6 | use rustyline::DefaultEditor; 7 | use std::str::FromStr; 8 | 9 | const HUMAN_PROMPT: &str = "🌴 ▶ "; 10 | 11 | pub const INTERACT_INSTRUCTIONS: &str = "INTERACTIVE MODE: 12 | 13 | Use subcommands in interactive mode directly. \ 14 | No OPTIONS (flags) of subcommands are understood in interactive mode. \ 15 | The ; character can be used to separate commands. 16 | 17 | The following additional commands are available: 18 | ? Show the short version of \"help\" 19 | clear Clear the terminal screen 20 | use Change to the specified stack [aliases: stack] 21 | exit Quit interactive mode [aliases: quit, q]"; 22 | 23 | pub const INTERACT_LONG_INSTRUCTIONS: &str = "INTERACTIVE MODE: 24 | 25 | Use subcommands in interactive mode directly. For example: 26 | 27 | 🌴 ▶ push a new thing 28 | Created: a new thing 29 | 🌴 ▶ peek 30 | Now: a new thing 31 | 🌴 ▶ delete 32 | Deleted: a new thing 33 | Now: nothing 34 | 🌴 ▶ exit 35 | exit: Buen biåhe! 36 | 37 | No OPTIONS (flags) of subcommands are understood in interactive mode. 38 | 39 | The ; character can be used to separate commands. 40 | 41 | In interactive mode, the following additional commands are available: 42 | ? 43 | Show the short version of \"help\" 44 | clear 45 | Clear the terminal screen 46 | use 47 | Change to the specified stack [aliases: stack] 48 | exit 49 | Quit interactive mode [aliases: quit, q]"; 50 | 51 | // TODO: pagination/scrollback? 52 | // TODO: more comprehensive tests 53 | pub fn interact(original_stack: String, data_store: DataStore, output: OutputFormat) { 54 | print_welcome_msg(output); 55 | 56 | let mut rl = DefaultEditor::new().expect("Unable to create readline."); 57 | let prompt = if output.is_nonquiet_for_humans() { 58 | HUMAN_PROMPT 59 | } else { 60 | "" 61 | }; 62 | 63 | let mut stack = original_stack; 64 | 65 | loop { 66 | let line = rl.readline(prompt); 67 | 68 | if let Ok(line) = &line { 69 | rl.add_history_entry(line).unwrap(); 70 | } 71 | 72 | use InteractAction::*; 73 | let line = line.map_err(handle_error).map(handle_line(&stack)); 74 | let actions = match line { 75 | Ok(actions) => actions, 76 | Err(err_action) => vec![err_action], 77 | }; 78 | 79 | for action in actions { 80 | match action { 81 | ShortHelp => Cli::command().print_help().unwrap(), 82 | LongHelp => Cli::command().print_long_help().unwrap(), 83 | Clear => clearscreen::clear().expect("Failed to clear screen"), 84 | DoEffect(effect) => effect.run(&data_store, &output), 85 | UseStack(new_stack) => { 86 | stack = new_stack; 87 | output.log(vec!["update", "stack"], vec![vec!["Active stack", &stack]]); 88 | } 89 | NoContent => (), 90 | Exit(reason) => { 91 | print_goodbye_msg(&reason, output); 92 | return; 93 | } 94 | MissingArgument(msg) => { 95 | output.log( 96 | vec!["argument", "error"], 97 | vec![vec![&msg, "missing argument"]], 98 | ); 99 | } 100 | Error(msg) => { 101 | output.log( 102 | vec!["exit-message", "exit-reason"], 103 | vec![vec!["Error"], vec![&msg]], 104 | ); 105 | return; 106 | } 107 | Unknown(term) => { 108 | if output.is_nonquiet_for_humans() { 109 | println!("Oops, I don't know {:?}", term); 110 | } else { 111 | output.log(vec!["term", "error"], vec![vec![&term, "unknown term"]]); 112 | }; 113 | } 114 | }; 115 | } 116 | } 117 | } 118 | 119 | fn print_welcome_msg(output: OutputFormat) { 120 | if output.is_nonquiet_for_humans() { 121 | println!("sigi {}", SIGI_VERSION); 122 | println!( 123 | "Type \"exit\", \"quit\", or \"q\" to quit. (On Unixy systems, Ctrl+C or Ctrl+D also work)" 124 | ); 125 | println!("Type \"?\" for quick help, or \"help\" for a more verbose help message."); 126 | println!(); 127 | } 128 | } 129 | 130 | fn print_goodbye_msg(reason: &str, output: OutputFormat) { 131 | output.log( 132 | vec!["exit-reason", "exit-message"], 133 | vec![vec![reason, "Buen biåhe!"]], 134 | ); 135 | } 136 | 137 | enum InteractAction { 138 | ShortHelp, 139 | LongHelp, 140 | Clear, 141 | DoEffect(StackEffect), 142 | UseStack(String), 143 | NoContent, 144 | Exit(String), 145 | MissingArgument(String), 146 | Error(String), 147 | Unknown(String), 148 | } 149 | 150 | fn handle_error(err: ReadlineError) -> InteractAction { 151 | match err { 152 | ReadlineError::Interrupted => InteractAction::Exit("Ctrl+c".to_string()), 153 | ReadlineError::Eof => InteractAction::Exit("Ctrl+d".to_string()), 154 | err => InteractAction::Error(format!("{:?}", err)), 155 | } 156 | } 157 | 158 | fn handle_line(stack: &str) -> impl Fn(String) -> Vec + '_ { 159 | |line| { 160 | line.split(';') 161 | .map(|s| s.to_string()) 162 | .map(|line| parse_line(line, stack.to_string())) 163 | .collect() 164 | } 165 | } 166 | 167 | fn parse_line(line: String, stack: String) -> InteractAction { 168 | let tokens = line.split_ascii_whitespace().collect::>(); 169 | 170 | if tokens.is_empty() { 171 | return InteractAction::NoContent; 172 | } 173 | 174 | let term = tokens.first().unwrap().to_ascii_lowercase(); 175 | 176 | match term.as_str() { 177 | "?" => InteractAction::ShortHelp, 178 | "help" => InteractAction::LongHelp, 179 | "clear" => InteractAction::Clear, 180 | "exit" | "quit" | "q" => InteractAction::Exit(term), 181 | "use" | "stack" => match tokens.get(1) { 182 | Some(stack) => InteractAction::UseStack(stack.to_string()), 183 | None => InteractAction::MissingArgument("stack name".to_string()), 184 | }, 185 | _ => match parse_effect(tokens, stack) { 186 | ParseEffectResult::Effect(effect) => InteractAction::DoEffect(effect), 187 | ParseEffectResult::NotEffect(parse_res) => parse_res, 188 | ParseEffectResult::Unknown => InteractAction::Unknown(term), 189 | }, 190 | } 191 | } 192 | 193 | enum ParseEffectResult { 194 | Effect(StackEffect), 195 | NotEffect(InteractAction), 196 | Unknown, 197 | } 198 | 199 | fn parse_effect(tokens: Vec<&str>, stack: String) -> ParseEffectResult { 200 | let term = tokens.first().unwrap_or(&""); 201 | 202 | let parse_n = || tokens.get(1).and_then(|&s| usize::from_str(s).ok()); 203 | 204 | use ParseEffectResult::*; 205 | use StackEffect::*; 206 | 207 | if COMPLETE_TERMS.contains(term) { 208 | let index = parse_n().unwrap_or(0); 209 | return Effect(Complete { stack, index }); 210 | } 211 | if COUNT_TERMS.contains(term) { 212 | return Effect(Count { stack }); 213 | } 214 | if DELETE_TERMS.contains(term) { 215 | let index = parse_n().unwrap_or(0); 216 | return Effect(Delete { stack, index }); 217 | } 218 | if DELETE_ALL_TERMS.contains(term) { 219 | return Effect(DeleteAll { stack }); 220 | } 221 | if EDIT_TERMS.contains(term) { 222 | let index = parse_n().unwrap_or(0); 223 | return Effect(Edit { 224 | stack, 225 | editor: resolve_editor(None), 226 | index, 227 | }); 228 | } 229 | if HEAD_TERMS.contains(term) { 230 | let n = parse_n().unwrap_or(DEFAULT_SHORT_LIST_LIMIT); 231 | return Effect(Head { stack, n }); 232 | } 233 | if IS_EMPTY_TERMS.contains(term) { 234 | return Effect(IsEmpty { stack }); 235 | } 236 | if LIST_TERMS.contains(term) { 237 | return Effect(ListAll { stack }); 238 | } 239 | if LIST_STACKS_TERMS.contains(term) { 240 | return Effect(ListStacks); 241 | } 242 | if MOVE_TERMS.contains(term) { 243 | match tokens.get(1) { 244 | Some(dest) => { 245 | let dest = dest.to_string(); 246 | return Effect(Move { stack, dest }); 247 | } 248 | None => { 249 | return NotEffect(InteractAction::MissingArgument( 250 | "destination stack".to_string(), 251 | )); 252 | } 253 | }; 254 | } 255 | if MOVE_ALL_TERMS.contains(term) { 256 | match tokens.get(1) { 257 | Some(dest) => { 258 | let dest = dest.to_string(); 259 | return Effect(MoveAll { stack, dest }); 260 | } 261 | None => { 262 | return NotEffect(InteractAction::MissingArgument( 263 | "destination stack".to_string(), 264 | )); 265 | } 266 | }; 267 | } 268 | if NEXT_TERMS.contains(term) { 269 | return Effect(Next { stack }); 270 | } 271 | if PEEK_TERMS.contains(term) { 272 | return Effect(Peek { stack }); 273 | } 274 | if PICK_TERMS.contains(term) { 275 | let indices = tokens 276 | .iter() 277 | .filter_map(|s| usize::from_str(s).ok()) 278 | .collect(); 279 | return Effect(Pick { stack, indices }); 280 | } 281 | if PUSH_TERMS.contains(term) { 282 | // FIXME: This is convenient, but normalizes whitespace. (E.g. multiple spaces always collapsed, tabs to spaces, etc) 283 | let content = tokens[1..].join(" "); 284 | return Effect(Push { stack, content }); 285 | } 286 | if ROT_TERMS.contains(term) { 287 | return Effect(Rot { stack }); 288 | } 289 | if SWAP_TERMS.contains(term) { 290 | return Effect(Swap { stack }); 291 | } 292 | if TAIL_TERMS.contains(term) { 293 | let n = parse_n().unwrap_or(DEFAULT_SHORT_LIST_LIMIT); 294 | return Effect(Tail { stack, n }); 295 | } 296 | 297 | Unknown 298 | } 299 | -------------------------------------------------------------------------------- /src/data.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::io::ErrorKind; 3 | use std::{env, fs, path::PathBuf}; 4 | 5 | use directories::ProjectDirs; 6 | 7 | // TODO: Alternate data stores: 8 | // - Redis 9 | // - SQLite 10 | // - The existing version (JSON via Serde) 11 | // TODO: Allow an idea of "stack of stacks" 12 | 13 | use chrono::{DateTime, Local}; 14 | use serde::{Deserialize, Serialize}; 15 | 16 | /// A stack of items. 17 | pub type Stack = Vec; 18 | 19 | type ItemHistory = Vec<(String, DateTime)>; 20 | 21 | /// A single stack item. 22 | #[derive(Serialize, Deserialize, Debug, Clone)] 23 | pub struct Item { 24 | pub contents: String, 25 | pub history: ItemHistory, 26 | } 27 | 28 | impl Item { 29 | pub fn new(contents: &str) -> Self { 30 | Item { 31 | contents: contents.to_string(), 32 | history: vec![("created".to_string(), Local::now())], 33 | } 34 | } 35 | 36 | pub fn mark_completed(&mut self) { 37 | let event = ("completed".to_string(), Local::now()); 38 | self.history.push(event); 39 | } 40 | 41 | pub fn mark_deleted(&mut self) { 42 | let event = ("deleted".to_string(), Local::now()); 43 | self.history.push(event); 44 | } 45 | 46 | pub fn mark_restored(&mut self) { 47 | let event = ("restored".to_string(), Local::now()); 48 | self.history.push(event); 49 | } 50 | } 51 | 52 | pub struct DataStore { 53 | pub working_dir: WorkingDir, 54 | pub data_format: DataFormat, 55 | } 56 | 57 | #[derive(Clone)] 58 | pub enum WorkingDir { 59 | HomeDir, 60 | Dir(String), 61 | // TODO: URI (?) 62 | } 63 | 64 | pub enum DataFormat { 65 | SigiJson, 66 | // TODO: SQLite 67 | // TODO: Redis(?) 68 | } 69 | 70 | impl DataStore { 71 | pub fn load(&self, stack_name: &str) -> Result { 72 | match self.data_format { 73 | DataFormat::SigiJson => load_json_from(stack_name, &self.dir()), 74 | } 75 | } 76 | 77 | pub fn save(&self, stack_name: &str, items: Stack) -> Result<(), impl Error> { 78 | match self.data_format { 79 | DataFormat::SigiJson => save_json_to(stack_name, &self.dir(), items), 80 | } 81 | } 82 | 83 | pub fn list_stacks(&self) -> Result, impl Error> { 84 | match self.data_format { 85 | DataFormat::SigiJson => list_json_from(&self.dir()), 86 | } 87 | } 88 | 89 | fn dir(&self) -> String { 90 | match self.working_dir.clone() { 91 | WorkingDir::HomeDir => sigi_path(), 92 | WorkingDir::Dir(dir) => dir, 93 | } 94 | } 95 | } 96 | 97 | /// Save a stack of items. 98 | // TODO: Create a custom error. This is returning raw filesystem errors. 99 | fn save_json_to(stack_name: &str, dest_dir: &str, items: Stack) -> Result<(), impl Error> { 100 | let data_path: String = sigi_file(dest_dir, stack_name); 101 | let json: String = serde_json::to_string(&items).unwrap(); 102 | let result = fs::write(&data_path, &json); 103 | if result.is_err() && result.as_ref().unwrap_err().kind() == ErrorKind::NotFound { 104 | fs::create_dir_all(dest_dir).unwrap(); 105 | fs::write(data_path, json) 106 | } else { 107 | result 108 | } 109 | } 110 | 111 | /// Load a stack of items. 112 | // TODO: Create a custom error. This is returning raw serialization errors. 113 | fn load_json_from(stack_name: &str, dest_dir: &str) -> Result { 114 | let data_path: String = sigi_file(dest_dir, stack_name); 115 | let read_result = fs::read_to_string(data_path); 116 | if read_result.is_err() && read_result.as_ref().unwrap_err().kind() == ErrorKind::NotFound { 117 | return Ok(vec![]); 118 | } 119 | 120 | let json = read_result.unwrap(); 121 | let result = serde_json::from_str(&json); 122 | 123 | if result.is_err() { 124 | let v1result = v1_load(&json); 125 | if let Ok(v1stack) = v1result { 126 | return Ok(v1_to_modern(v1stack)); 127 | } 128 | } 129 | 130 | result 131 | } 132 | 133 | fn list_json_from(dest_dir: &str) -> Result, impl Error> { 134 | let dot_json = ".json"; 135 | fs::read_dir(dest_dir).map(|files| { 136 | files 137 | .map(|file| file.unwrap().file_name().into_string().unwrap()) 138 | .filter(|filename| filename.ends_with(dot_json)) 139 | .map(|filename| filename.strip_suffix(dot_json).unwrap().to_string()) 140 | .collect::>() 141 | }) 142 | } 143 | 144 | fn v1_sigi_path() -> PathBuf { 145 | let home = env::var("HOME").or_else(|_| env::var("HOMEDRIVE")).unwrap(); 146 | let path = format!("{}/.local/share/sigi", home); 147 | PathBuf::from(&path) 148 | } 149 | 150 | fn sigi_path() -> String { 151 | if let Ok(dir) = env::var("SIGI_HOME") { 152 | return dir; 153 | } 154 | 155 | let sigi_base = ProjectDirs::from("org", "sigi-cli", "sigi").unwrap(); 156 | let sigi_path = sigi_base.data_dir(); 157 | let v1_path = v1_sigi_path(); 158 | 159 | if v1_path.exists() && !sigi_path.exists() { 160 | fs::rename(v1_path, sigi_path).unwrap(); 161 | } 162 | 163 | sigi_path.to_string_lossy().to_string() 164 | } 165 | 166 | fn sigi_file(sigi_dir: &str, filename: &str) -> String { 167 | let path = format!("{}/{}.json", sigi_dir, filename); 168 | PathBuf::from(&path).to_string_lossy().to_string() 169 | } 170 | 171 | /// A single stack item. Used for backwards compatibility with versions of Sigi v1. 172 | #[derive(Serialize, Deserialize, Debug, Clone)] 173 | struct V1Item { 174 | name: String, 175 | created: DateTime, 176 | succeeded: Option>, 177 | failed: Option>, 178 | } 179 | 180 | /// A stack of items. Used for backwards compatibility with versions of Sigi v1. 181 | type V1Stack = Vec; 182 | 183 | /// Attempt to read a V1 format file. 184 | fn v1_load(json_blob: &str) -> Result { 185 | serde_json::from_str(json_blob) 186 | } 187 | 188 | fn v1_to_modern(v1stack: V1Stack) -> Stack { 189 | v1stack 190 | .into_iter() 191 | .map(|v1item| { 192 | // Translate the old keys to entries. 193 | let mut history: ItemHistory = vec![ 194 | Some(("created", v1item.created)), 195 | v1item.succeeded.map(|dt| ("completed", dt)), 196 | v1item.failed.map(|dt| ("deleted", dt)), 197 | ] 198 | .into_iter() 199 | .flatten() 200 | .map(|(s, dt)| (s.to_string(), dt)) 201 | .collect(); 202 | history.sort_by_key(|(_, dt)| *dt); 203 | Item { 204 | contents: v1item.name, 205 | history, 206 | } 207 | }) 208 | .collect() 209 | } 210 | -------------------------------------------------------------------------------- /src/effects.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use chrono::Local; 4 | 5 | use crate::data::{DataStore, Item}; 6 | use crate::output::OutputFormat; 7 | 8 | const HISTORY_SUFFIX: &str = "_history"; 9 | 10 | // TODO: Consider more shuffle words: https://docs.factorcode.org/content/article-shuffle-words.html 11 | 12 | pub enum StackEffect { 13 | Push { 14 | stack: String, 15 | content: String, 16 | }, 17 | Complete { 18 | stack: String, 19 | index: usize, 20 | }, 21 | Delete { 22 | stack: String, 23 | index: usize, 24 | }, 25 | DeleteAll { 26 | stack: String, 27 | }, 28 | Edit { 29 | stack: String, 30 | editor: String, 31 | index: usize, 32 | }, 33 | Pick { 34 | stack: String, 35 | indices: Vec, 36 | }, 37 | Move { 38 | stack: String, 39 | dest: String, 40 | }, 41 | MoveAll { 42 | stack: String, 43 | dest: String, 44 | }, 45 | Swap { 46 | stack: String, 47 | }, 48 | Rot { 49 | stack: String, 50 | }, 51 | Next { 52 | stack: String, 53 | }, 54 | Peek { 55 | stack: String, 56 | }, 57 | ListAll { 58 | stack: String, 59 | }, 60 | ListStacks, 61 | Head { 62 | stack: String, 63 | n: usize, 64 | }, 65 | Tail { 66 | stack: String, 67 | n: usize, 68 | }, 69 | Count { 70 | stack: String, 71 | }, 72 | IsEmpty { 73 | stack: String, 74 | }, 75 | } 76 | 77 | impl StackEffect { 78 | pub fn run(self, data_store: &DataStore, output: &OutputFormat) { 79 | use StackEffect::*; 80 | match self { 81 | Push { stack, content } => push_content(stack, content, data_store, output), 82 | Complete { stack, index } => complete_item(stack, index, data_store, output), 83 | Delete { stack, index } => delete_latest_item(stack, index, data_store, output), 84 | DeleteAll { stack } => delete_all_items(stack, data_store, output), 85 | Edit { 86 | stack, 87 | editor, 88 | index, 89 | } => edit_item(stack, editor, index, data_store, output), 90 | Pick { stack, indices } => pick_indices(stack, indices, data_store, output), 91 | Move { stack, dest } => move_latest_item(stack, dest, data_store, output), 92 | MoveAll { stack, dest } => move_all_items(stack, dest, data_store, output), 93 | Swap { stack } => swap_latest_two_items(stack, data_store, output), 94 | Rot { stack } => rotate_latest_three_items(stack, data_store, output), 95 | Next { stack } => next_to_latest(stack, data_store, output), 96 | Peek { stack } => peek_latest_item(stack, data_store, output), 97 | ListAll { stack } => list_all_items(stack, data_store, output), 98 | ListStacks => list_stacks(data_store, output), 99 | Head { stack, n } => list_n_latest_items(stack, n, data_store, output), 100 | Tail { stack, n } => list_n_oldest_items(stack, n, data_store, output), 101 | Count { stack } => count_all_items(stack, data_store, output), 102 | IsEmpty { stack } => is_empty(stack, data_store, output), 103 | } 104 | } 105 | } 106 | 107 | fn push_content(stack: String, content: String, data_store: &DataStore, output: &OutputFormat) { 108 | let item = Item::new(&content); 109 | push_item(stack, item, data_store, output); 110 | } 111 | 112 | fn push_item(stack: String, item: Item, data_store: &DataStore, output: &OutputFormat) { 113 | let contents = item.contents.clone(); 114 | 115 | let items = if let Ok(items) = data_store.load(&stack) { 116 | let mut items = items; 117 | items.push(item); 118 | items 119 | } else { 120 | vec![item] 121 | }; 122 | 123 | data_store.save(&stack, items).unwrap(); 124 | 125 | output.log(vec!["action", "item"], vec![vec!["Created", &contents]]); 126 | } 127 | 128 | fn complete_item(stack: String, index: usize, data_store: &DataStore, output: &OutputFormat) { 129 | if let Ok(items) = data_store.load(&stack) { 130 | let mut items = items; 131 | 132 | if items.len() > index { 133 | let mut item = items.remove(items.len() - index - 1); 134 | item.mark_completed(); 135 | 136 | // Push the now-marked-completed item to history stack. 137 | push_item( 138 | stack_history_of(&stack), 139 | item.clone(), 140 | data_store, 141 | &OutputFormat::Silent, 142 | ); 143 | 144 | // Save the original stack without that item. 145 | data_store.save(&stack, items).unwrap(); 146 | 147 | output.log( 148 | vec!["action", "item"], 149 | vec![vec!["Completed", &item.contents]], 150 | ); 151 | } 152 | } 153 | 154 | if output.is_nonquiet_for_humans() { 155 | peek_latest_item(stack, data_store, output); 156 | } 157 | } 158 | 159 | fn delete_latest_item(stack: String, index: usize, data_store: &DataStore, output: &OutputFormat) { 160 | if let Ok(items) = data_store.load(&stack) { 161 | let mut items = items; 162 | 163 | if items.len() > index { 164 | let mut item = items.remove(items.len() - index - 1); 165 | item.mark_deleted(); 166 | 167 | // Push the now-marked-deleted item to history stack. 168 | push_item( 169 | stack_history_of(&stack), 170 | item.clone(), 171 | data_store, 172 | &OutputFormat::Silent, 173 | ); 174 | 175 | // Save the original stack without that item. 176 | data_store.save(&stack, items).unwrap(); 177 | 178 | output.log( 179 | vec!["action", "item"], 180 | vec![vec!["Deleted", &item.contents]], 181 | ); 182 | } 183 | } 184 | 185 | if output.is_nonquiet_for_humans() { 186 | peek_latest_item(stack, data_store, output); 187 | } 188 | } 189 | 190 | fn delete_all_items(stack: String, data_store: &DataStore, output: &OutputFormat) { 191 | if let Ok(items) = data_store.load(&stack) { 192 | let mut items = items; 193 | items.iter_mut().for_each(|item| item.mark_deleted()); 194 | let n_deleted = items.len(); 195 | 196 | // Push the now-marked-deleted items to history stack. 197 | let history_stack = &stack_history_of(&stack); 198 | let mut history = data_store.load(history_stack).unwrap_or_default(); 199 | history.append(&mut items); 200 | data_store.save(history_stack, history).unwrap(); 201 | 202 | // Save the original stack as empty now. 203 | data_store.save(&stack, vec![]).unwrap(); 204 | 205 | output.log( 206 | vec!["action", "item"], 207 | vec![vec!["Deleted", &format!("{} items", n_deleted)]], 208 | ); 209 | } 210 | } 211 | 212 | fn edit_item( 213 | stack: String, 214 | editor: String, 215 | index: usize, 216 | data_store: &DataStore, 217 | output: &OutputFormat, 218 | ) { 219 | if let Ok(items) = data_store.load(&stack) { 220 | let mut items = items; 221 | if index < items.len() { 222 | let tmp = std::env::temp_dir().as_path().join("sigi"); 223 | std::fs::create_dir_all(&tmp).unwrap_or_else(|err| { 224 | panic!( 225 | "Unable to create temporary directory {:?} for editing: {}", 226 | tmp, err 227 | ) 228 | }); 229 | let tmpfile = tmp.as_path().join(Local::now().timestamp().to_string()); 230 | std::fs::write(&tmpfile, &items[index].contents).unwrap_or_else(|err| { 231 | panic!( 232 | "Unable to write to temporary file {:?} for editing: {}", 233 | tmpfile, err 234 | ) 235 | }); 236 | 237 | let editor = editor.split_whitespace().collect::>(); 238 | 239 | let edit_exit_code = Command::new(editor[0]) 240 | .args(&editor[1..]) 241 | .arg(&tmpfile) 242 | .status() 243 | .unwrap_or_else(|err| panic!("Failed to execute {:?} editor: {}", editor, err)); 244 | 245 | if edit_exit_code.success() { 246 | let new_content = std::fs::read_to_string(&tmpfile).unwrap_or_else(|err| { 247 | panic!( 248 | "Unable to read from temporary file {:?} after editing: {}", 249 | tmpfile, err 250 | ) 251 | }); 252 | items[index].contents.clone_from(&new_content); 253 | 254 | data_store.save(&stack, items).unwrap(); 255 | 256 | output.log(vec!["action", "item"], vec![vec!["Edited", &new_content]]); 257 | } 258 | } 259 | } 260 | } 261 | 262 | fn pick_indices(stack: String, indices: Vec, data_store: &DataStore, output: &OutputFormat) { 263 | if let Ok(items) = data_store.load(&stack) { 264 | let mut items = items; 265 | let mut seen: Vec = vec![]; 266 | seen.reserve_exact(indices.len()); 267 | let indices: Vec = indices.iter().map(|i| items.len() - 1 - i).rev().collect(); 268 | for i in indices { 269 | if i > items.len() || seen.contains(&i) { 270 | // TODO: What should be the output here? Some stderr? 271 | // command.log("Pick", "ignoring out-of-bounds index"); 272 | // command.log("Pick", "ignoring duplicate index"); 273 | continue; 274 | } 275 | let i = i - seen.iter().filter(|j| j < &&i).count(); 276 | let picked = items.remove(i); 277 | items.push(picked); 278 | seen.push(i); 279 | } 280 | 281 | data_store.save(&stack, items).unwrap(); 282 | 283 | if output.is_nonquiet_for_humans() { 284 | list_n_latest_items(stack, seen.len(), data_store, output); 285 | } 286 | } 287 | } 288 | 289 | fn move_latest_item(source: String, dest: String, data_store: &DataStore, output: &OutputFormat) { 290 | if let Ok(items) = data_store.load(&source) { 291 | let mut items = items; 292 | if let Some(item) = items.pop() { 293 | data_store.save(&source, items).unwrap(); 294 | 295 | output.log( 296 | vec!["action", "new-stack", "old-stack"], 297 | vec![vec!["Move", &dest, &source]], 298 | ); 299 | 300 | push_item(dest, item, data_store, &OutputFormat::Silent); 301 | } 302 | } 303 | } 304 | 305 | fn move_all_items(source: String, dest: String, data_store: &DataStore, output: &OutputFormat) { 306 | if let Ok(src_items) = data_store.load(&source) { 307 | let count = src_items.len(); 308 | 309 | if !src_items.is_empty() { 310 | let all_items = match data_store.load(&dest) { 311 | Ok(dest_items) => { 312 | let mut all_items = dest_items; 313 | for item in src_items { 314 | all_items.push(item); 315 | } 316 | all_items 317 | } 318 | _ => src_items, 319 | }; 320 | 321 | data_store.save(&dest, all_items).unwrap(); 322 | data_store.save(&source, vec![]).unwrap(); 323 | } 324 | 325 | output.log( 326 | vec!["action", "new-stack", "old-stack", "num-moved"], 327 | vec![vec!["Move All", &dest, &source, &count.to_string()]], 328 | ); 329 | } 330 | } 331 | 332 | fn swap_latest_two_items(stack: String, data_store: &DataStore, output: &OutputFormat) { 333 | if let Ok(items) = data_store.load(&stack) { 334 | let mut items = items; 335 | 336 | if items.len() < 2 { 337 | return; 338 | } 339 | 340 | let a = items.pop().unwrap(); 341 | let b = items.pop().unwrap(); 342 | items.push(a); 343 | items.push(b); 344 | 345 | data_store.save(&stack, items).unwrap(); 346 | 347 | if output.is_nonquiet_for_humans() { 348 | list_n_latest_items(stack, 2, data_store, output); 349 | } 350 | } 351 | } 352 | 353 | fn rotate_latest_three_items(stack: String, data_store: &DataStore, output: &OutputFormat) { 354 | if let Ok(items) = data_store.load(&stack) { 355 | let mut items = items; 356 | 357 | if items.len() < 3 { 358 | swap_latest_two_items(stack, data_store, output); 359 | return; 360 | } 361 | 362 | let a = items.pop().unwrap(); 363 | let b = items.pop().unwrap(); 364 | let c = items.pop().unwrap(); 365 | 366 | items.push(a); 367 | items.push(c); 368 | items.push(b); 369 | 370 | data_store.save(&stack, items).unwrap(); 371 | 372 | if output.is_nonquiet_for_humans() { 373 | list_n_latest_items(stack, 3, data_store, output); 374 | } 375 | } 376 | } 377 | 378 | fn next_to_latest(stack: String, data_store: &DataStore, output: &OutputFormat) { 379 | if let Ok(items) = data_store.load(&stack) { 380 | let mut items = items; 381 | if items.is_empty() { 382 | return; 383 | } 384 | let to_the_back = items.pop().unwrap(); 385 | items.insert(0, to_the_back); 386 | 387 | data_store.save(&stack, items).unwrap(); 388 | 389 | if output.is_nonquiet_for_humans() { 390 | peek_latest_item(stack, data_store, output); 391 | } 392 | } 393 | } 394 | 395 | fn peek_latest_item(stack: String, data_store: &DataStore, output: &OutputFormat) { 396 | if let OutputFormat::Silent = output { 397 | return; 398 | } 399 | 400 | if let Ok(items) = data_store.load(&stack) { 401 | let top_item = items.last().map(|i| i.contents.as_str()); 402 | 403 | let output_it = |it| output.log_always(vec!["position", "item"], it); 404 | 405 | match top_item { 406 | Some(contents) => output_it(vec![vec!["Now", contents]]), 407 | None => { 408 | if output.is_nonquiet_for_humans() { 409 | output_it(vec![vec!["Now", "NOTHING"]]) 410 | } else { 411 | output_it(vec![]) 412 | } 413 | } 414 | } 415 | } 416 | } 417 | 418 | fn count_all_items(stack: String, data_store: &DataStore, output: &OutputFormat) { 419 | if let OutputFormat::Silent = output { 420 | return; 421 | } 422 | 423 | if let Ok(items) = data_store.load(&stack) { 424 | let len = items.len().to_string(); 425 | output.log_always(vec!["items"], vec![vec![&len]]) 426 | } 427 | } 428 | 429 | fn is_empty(stack: String, data_store: &DataStore, output: &OutputFormat) { 430 | if let Ok(items) = data_store.load(&stack) { 431 | if !items.is_empty() { 432 | output.log_always(vec!["empty"], vec![vec!["false"]]); 433 | // Exit with a failure (nonzero status) when not empty. 434 | // This helps people who do shell scripting do something like: 435 | // while ! sigi -t $stack is-empty ; do ; done 436 | // TODO: It would be better modeled as an error, if anyone uses as a lib this will surprise. 437 | if let OutputFormat::TerseText = output { 438 | return; 439 | } else { 440 | std::process::exit(1); 441 | } 442 | } 443 | } 444 | output.log_always(vec!["empty"], vec![vec!["true"]]); 445 | } 446 | 447 | fn list_stacks(data_store: &DataStore, output: &OutputFormat) { 448 | if let Ok(stacks) = data_store.list_stacks() { 449 | let mut stacks = stacks; 450 | stacks.sort(); 451 | let strs = stacks.iter().map(|stack| vec![stack.as_str()]).collect(); 452 | output.log_always(vec!["stack"], strs); 453 | } 454 | } 455 | 456 | // ===== ListAll/Head/Tail ===== 457 | 458 | struct ListRange { 459 | stack: String, 460 | // Ignored if starting "from_end". 461 | start: usize, 462 | limit: Option, 463 | from_end: bool, 464 | } 465 | 466 | fn list_range(range: ListRange, data_store: &DataStore, output: &OutputFormat) { 467 | if let OutputFormat::Silent = output { 468 | return; 469 | } 470 | 471 | if let Ok(items) = data_store.load(&range.stack) { 472 | let limit = match range.limit { 473 | Some(n) => n, 474 | None => items.len(), 475 | }; 476 | 477 | let start = if range.from_end { 478 | if limit <= items.len() { 479 | items.len() - limit 480 | } else { 481 | 0 482 | } 483 | } else { 484 | range.start 485 | }; 486 | 487 | let lines = items 488 | .into_iter() 489 | .rev() 490 | .enumerate() 491 | .skip(start) 492 | .take(limit) 493 | .map(|(i, item)| { 494 | // Pad human output numbers to line up nicely with "Now". 495 | let position = if output.is_nonquiet_for_humans() { 496 | match i { 497 | 0 => "Now".to_string(), 498 | 1..=9 => format!(" {}", i), 499 | 10..=99 => format!(" {}", i), 500 | _ => i.to_string(), 501 | } 502 | } else { 503 | i.to_string() 504 | }; 505 | 506 | let created = item 507 | .history 508 | .iter() 509 | .find(|(status, _)| status == "created") 510 | .map(|(_, dt)| output.format_time(*dt)) 511 | .unwrap_or_else(|| "unknown".to_string()); 512 | 513 | vec![position, item.contents, created] 514 | }) 515 | .collect::>(); 516 | 517 | let labels = vec!["position", "item", "created"]; 518 | 519 | if lines.is_empty() { 520 | if output.is_nonquiet_for_humans() { 521 | output.log(labels, vec![vec!["Now", "NOTHING"]]); 522 | } 523 | return; 524 | } 525 | 526 | // Get the lines into a "borrow" state (&str instead of String) to make log happy. 527 | let lines = lines 528 | .iter() 529 | .map(|line| line.iter().map(|s| s.as_str()).collect()) 530 | .collect(); 531 | 532 | output.log_always(labels, lines); 533 | } 534 | } 535 | 536 | fn list_all_items(stack: String, data_store: &DataStore, output: &OutputFormat) { 537 | let range = ListRange { 538 | stack, 539 | start: 0, 540 | limit: None, 541 | from_end: false, 542 | }; 543 | 544 | list_range(range, data_store, output); 545 | } 546 | 547 | fn list_n_latest_items(stack: String, n: usize, data_store: &DataStore, output: &OutputFormat) { 548 | let range = ListRange { 549 | stack, 550 | start: 0, 551 | limit: Some(n), 552 | from_end: false, 553 | }; 554 | 555 | list_range(range, data_store, output); 556 | } 557 | 558 | fn list_n_oldest_items(stack: String, n: usize, data_store: &DataStore, output: &OutputFormat) { 559 | let range = ListRange { 560 | stack, 561 | start: 0, 562 | limit: Some(n), 563 | from_end: true, 564 | }; 565 | 566 | list_range(range, data_store, output); 567 | } 568 | 569 | // ===== Helper functions ===== 570 | 571 | fn stack_history_of(stack: &str) -> String { 572 | stack.to_string() + HISTORY_SUFFIX 573 | } 574 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Sigi: An organizing CLI. 2 | //! 3 | //! The CLI and usage is documented briefly on the main GitHub project here: 4 | //! 5 | //! - https://github.com/sigi-cli/sigi 6 | //! 7 | //! Its "database" is currently little more than json files, and handles only 8 | //! String values. It can work for research or small loads, but would be 9 | //! sluggish for anything that needs to care about performance. Other data 10 | //! stores like Redis and SQLite are planned. 11 | //! 12 | //! Other internals are documented, but the project is early in development 13 | //! and should be considered **unstable** at best. 14 | 15 | // TODO: Add guidance and examples for using sigi as a library... Or stop being a library. 16 | 17 | /// The main interface of Sigi, stack (and stack-adjacent) actions. 18 | pub mod effects; 19 | 20 | /// The CLI implementation. 21 | pub mod cli; 22 | 23 | /// The item, stack, and persistence implementation. 24 | pub mod data; 25 | 26 | /// The printing implementation. 27 | pub mod output; 28 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | //! The general idea in this module is to take a table-ish output and render it in common formats. 2 | //! 3 | //! ```text 4 | //! labels: [a, b, c] 5 | //! values:[[1, 2, 3], 6 | //! [4, 5, 6]] 7 | //! ``` 8 | //! 9 | //! For example, as json: 10 | //! ```json 11 | //! [ 12 | //! { 13 | //! "a": "1", 14 | //! "b": "2", 15 | //! "c": "3" 16 | //! }, 17 | //! { 18 | //! "a": "4", 19 | //! "b": "5", 20 | //! "c": "6" 21 | //! } 22 | //! ] 23 | //! ``` 24 | 25 | use chrono::{DateTime, Local}; 26 | 27 | /// Output formats supported by Sigi. 28 | #[derive(Clone, Copy, Eq, PartialEq)] 29 | pub enum OutputFormat { 30 | /// Comma-separated values. 31 | Csv, 32 | /// Human readable formats. Accepts a "noise level" for how much to output. 33 | Human(NoiseLevel), 34 | /// JSON (JavaScript Object Notation) - Pretty-printed with newlines and two-space indentation. 35 | Json, 36 | /// JSON (JavaScript Object Notation) - No newlines or indentation. 37 | JsonCompact, 38 | /// Print nothing at all. 39 | Silent, 40 | /// Print only on printing actions. 41 | TerseText, 42 | /// Tab-separated values. 43 | Tsv, 44 | } 45 | 46 | /// How much noise (verbosity) should be used when printing to standard output. 47 | #[derive(Clone, Copy, Eq, PartialEq)] 48 | pub enum NoiseLevel { 49 | Verbose, 50 | Normal, 51 | Quiet, 52 | } 53 | 54 | impl OutputFormat { 55 | pub fn format_time(&self, dt: DateTime) -> String { 56 | // TODO: This should be configurable. 57 | // TODO: Does this work for all locales? 58 | dt.to_rfc2822() 59 | } 60 | 61 | pub fn is_nonquiet_for_humans(&self) -> bool { 62 | match self { 63 | OutputFormat::Human(NoiseLevel::Quiet) => false, 64 | OutputFormat::Human(_) => true, 65 | _ => false, 66 | } 67 | } 68 | 69 | // TODO: Vec to slice: Vec<&str> -> &[&str] and Vec> -> &[&[&str]] 70 | // TODO: Or... some better intermediate format 71 | pub fn log_always(&self, labels: Vec<&str>, values: Vec>) { 72 | if let OutputFormat::TerseText = self { 73 | quiet_print(values); 74 | } else { 75 | self.log(labels, values); 76 | } 77 | } 78 | 79 | // TODO: Vec to slice: Vec<&str> -> &[&str] and Vec> -> &[&[&str]] 80 | // TODO: Or... some better intermediate format 81 | pub fn log(&self, labels: Vec<&str>, values: Vec>) { 82 | if let OutputFormat::Silent = self { 83 | return; 84 | } 85 | if let OutputFormat::TerseText = self { 86 | return; 87 | } 88 | 89 | match &self { 90 | OutputFormat::Csv => { 91 | let print_csv = join_and_print(","); 92 | print_csv(labels); 93 | values.into_iter().for_each(print_csv) 94 | } 95 | OutputFormat::Human(noise) => match noise { 96 | NoiseLevel::Verbose => { 97 | values.into_iter().for_each(|line| match line.len() { 98 | 0 => (), 99 | 1 => println!("{}", line.first().unwrap()), 100 | 2 => println!("{}: {}", line.first().unwrap(), line.get(1).unwrap()), 101 | _ => println!( 102 | "{}: {} ({})", 103 | line.first().unwrap(), 104 | line.get(1).unwrap(), 105 | line.iter() 106 | .skip(2) 107 | .map(|s| s.to_string()) 108 | .collect::>() 109 | .join(", ") 110 | ), 111 | }); 112 | } 113 | NoiseLevel::Normal => { 114 | // Print only first two values e.g. (num, item) separated by a single space. 115 | values.into_iter().for_each(|line| { 116 | if let (Some(label), Some(item)) = (line.first(), line.get(1)) { 117 | println!("{}: {}", label, item); 118 | } else if let Some(info) = line.first() { 119 | println!("{}", info); 120 | } 121 | }); 122 | } 123 | NoiseLevel::Quiet => quiet_print(values), 124 | }, 125 | OutputFormat::Json => { 126 | let keys = labels; 127 | let objs = values 128 | .into_iter() 129 | .map(|vals| { 130 | let mut obj = json::JsonValue::new_object(); 131 | keys.iter().zip(vals).for_each(|(k, v)| obj[*k] = v.into()); 132 | obj 133 | }) 134 | .collect::>(); 135 | 136 | println!("{}", json::stringify_pretty(objs, 2)); 137 | } 138 | OutputFormat::JsonCompact => { 139 | let keys = labels; 140 | let objs = values 141 | .into_iter() 142 | .map(|vals| { 143 | let mut obj = json::JsonValue::new_object(); 144 | keys.iter().zip(vals).for_each(|(k, v)| obj[*k] = v.into()); 145 | obj 146 | }) 147 | .collect::>(); 148 | 149 | println!("{}", json::stringify(objs)); 150 | } 151 | OutputFormat::Silent => { 152 | unreachable!("[BUG] Sigi should always exit outputting before this point.") 153 | } 154 | OutputFormat::TerseText => { 155 | unreachable!("[BUG] Sigi should always exit outputting before this point.") 156 | } 157 | OutputFormat::Tsv => { 158 | let print_tsv = join_and_print("\t"); 159 | print_tsv(labels); 160 | values.into_iter().for_each(print_tsv) 161 | } 162 | } 163 | } 164 | } 165 | 166 | fn quiet_print(values: Vec>) { 167 | values.into_iter().for_each(|line| { 168 | // Print only second value (item) separated by a single space. 169 | if let Some(message) = line.get(1) { 170 | println!("{}", message); 171 | } else if let Some(message) = line.first() { 172 | println!("{}", message); 173 | } 174 | }) 175 | } 176 | 177 | fn join_and_print(sep: &str) -> impl Fn(Vec<&str>) { 178 | let sep = sep.to_string(); 179 | move |tokens: Vec<&str>| println!("{}", tokens.join(&sep)) 180 | } 181 | -------------------------------------------------------------------------------- /tests/abcd_tests.rs: -------------------------------------------------------------------------------- 1 | mod run_sigi; 2 | 3 | use run_sigi::sigi; 4 | 5 | #[test] 6 | fn sigi_abcd_tests() { 7 | let stack = "_integ::abc"; 8 | 9 | let res = sigi(stack, &["delete-all"]); 10 | res.assert_success(); 11 | 12 | // ['a b c'] 13 | let res = sigi(stack, &["push", "a", "b", "c"]); 14 | res.assert_success(); 15 | res.assert_stdout_eq("Created: a b c\n"); 16 | res.assert_stderr_empty(); 17 | 18 | let res = sigi(stack, &["count"]); 19 | res.assert_success(); 20 | res.assert_stdout_eq("1\n"); 21 | res.assert_stderr_empty(); 22 | 23 | let res = sigi(stack, &["delete"]); 24 | res.assert_success(); 25 | res.assert_stdout_lines_eq(&["Deleted: a b c", "Now: NOTHING"]); 26 | res.assert_stderr_empty(); 27 | 28 | // ['a'] 29 | let res = sigi(stack, &["push", "a"]); 30 | res.assert_success(); 31 | res.assert_stdout_eq("Created: a\n"); 32 | res.assert_stderr_empty(); 33 | 34 | let res = sigi(stack, &["peek"]); 35 | res.assert_success(); 36 | res.assert_stdout_eq("Now: a\n"); 37 | res.assert_stderr_empty(); 38 | 39 | let res = sigi(stack, &["list"]); 40 | res.assert_success(); 41 | res.assert_stdout_eq("Now: a\n"); 42 | res.assert_stderr_empty(); 43 | 44 | // ['a', 'b'] 45 | let res = sigi(stack, &["push", "b"]); 46 | res.assert_success(); 47 | res.assert_stdout_eq("Created: b\n"); 48 | res.assert_stderr_empty(); 49 | 50 | let res = sigi(stack, &["peek"]); 51 | res.assert_success(); 52 | res.assert_stdout_eq("Now: b\n"); 53 | res.assert_stderr_empty(); 54 | 55 | let res = sigi(stack, &["list"]); 56 | res.assert_success(); 57 | res.assert_stdout_lines_eq(&["Now: b", " 1: a"]); 58 | res.assert_stderr_empty(); 59 | 60 | // ['a', 'b', 'c'] 61 | let res = sigi(stack, &["push", "c"]); 62 | res.assert_success(); 63 | res.assert_stdout_eq("Created: c\n"); 64 | res.assert_stderr_empty(); 65 | 66 | let res = sigi(stack, &["peek"]); 67 | res.assert_success(); 68 | res.assert_stdout_eq("Now: c\n"); 69 | res.assert_stderr_empty(); 70 | 71 | let res = sigi(stack, &["list"]); 72 | res.assert_success(); 73 | res.assert_stdout_lines_eq(&["Now: c", " 1: b", " 2: a"]); 74 | res.assert_stderr_empty(); 75 | 76 | // ['a', 'b', 'c', 'd'] 77 | let res = sigi(stack, &["push", "d"]); 78 | res.assert_success(); 79 | res.assert_stdout_eq("Created: d\n"); 80 | res.assert_stderr_empty(); 81 | 82 | let res = sigi(stack, &["peek"]); 83 | res.assert_success(); 84 | res.assert_stdout_eq("Now: d\n"); 85 | res.assert_stderr_empty(); 86 | 87 | let res = sigi(stack, &["list"]); 88 | res.assert_success(); 89 | res.assert_stdout_lines_eq(&["Now: d", " 1: c", " 2: b", " 3: a"]); 90 | res.assert_stderr_empty(); 91 | 92 | // swap 93 | let res = sigi(stack, &["swap"]); 94 | res.assert_success(); 95 | res.assert_stdout_lines_eq(&["Now: c", " 1: d", " 2: b", " 3: a"]); 96 | res.assert_stderr_empty(); 97 | 98 | let res = sigi(stack, &["swap"]); 99 | res.assert_success(); 100 | res.assert_stdout_lines_eq(&["Now: d", " 1: c", " 2: b", " 3: a"]); 101 | res.assert_stderr_empty(); 102 | 103 | // rot 104 | let res = sigi(stack, &["rot"]); 105 | res.assert_success(); 106 | res.assert_stdout_lines_eq(&["Now: c", " 1: b", " 2: d", " 3: a"]); 107 | res.assert_stderr_empty(); 108 | 109 | let res = sigi(stack, &["rot"]); 110 | res.assert_success(); 111 | res.assert_stdout_lines_eq(&["Now: b", " 1: d", " 2: c", " 3: a"]); 112 | res.assert_stderr_empty(); 113 | 114 | let res = sigi(stack, &["rot"]); 115 | res.assert_success(); 116 | res.assert_stdout_lines_eq(&["Now: d", " 1: c", " 2: b", " 3: a"]); 117 | res.assert_stderr_empty(); 118 | 119 | // next 120 | let res = sigi(stack, &["next"]); 121 | res.assert_success(); 122 | res.assert_stdout_lines_eq(&["Now: c", " 1: b", " 2: a", " 3: d"]); 123 | res.assert_stderr_empty(); 124 | 125 | let res = sigi(stack, &["next"]); 126 | res.assert_success(); 127 | res.assert_stdout_lines_eq(&["Now: b", " 1: a", " 2: d", " 3: c"]); 128 | res.assert_stderr_empty(); 129 | 130 | let res = sigi(stack, &["next"]); 131 | res.assert_success(); 132 | res.assert_stdout_lines_eq(&["Now: a", " 1: d", " 2: c", " 3: b"]); 133 | res.assert_stderr_empty(); 134 | 135 | let res = sigi(stack, &["next"]); 136 | res.assert_success(); 137 | res.assert_stdout_lines_eq(&["Now: d", " 1: c", " 2: b", " 3: a"]); 138 | res.assert_stderr_empty(); 139 | 140 | // removal tests 141 | let res = sigi(stack, &["delete"]); 142 | res.assert_success(); 143 | res.assert_stdout_lines_eq(&["Deleted: d", "Now: c"]); 144 | res.assert_stderr_empty(); 145 | 146 | let res = sigi(stack, &["complete", "1"]); 147 | res.assert_success(); 148 | res.assert_stdout_lines_eq(&["Completed: b", "Now: c"]); 149 | res.assert_stderr_empty(); 150 | 151 | let res = sigi(stack, &["complete"]); 152 | res.assert_success(); 153 | res.assert_stdout_lines_eq(&["Completed: c", "Now: a"]); 154 | res.assert_stderr_empty(); 155 | 156 | let res = sigi(stack, &["add", "b"]); 157 | res.assert_success(); 158 | res.assert_stdout_lines_eq(&["Created: b"]); 159 | res.assert_stderr_empty(); 160 | 161 | let res = sigi(stack, &["add", "c"]); 162 | res.assert_success(); 163 | res.assert_stdout_lines_eq(&["Created: c"]); 164 | res.assert_stderr_empty(); 165 | 166 | let res = sigi(stack, &["delete-all"]); 167 | res.assert_success(); 168 | res.assert_stdout_eq("Deleted: 3 items\n"); 169 | res.assert_stderr_empty(); 170 | } 171 | -------------------------------------------------------------------------------- /tests/basic_sigi_tests.rs: -------------------------------------------------------------------------------- 1 | mod run_sigi; 2 | 3 | use run_sigi::sigi; 4 | 5 | #[test] 6 | fn sigi_version() { 7 | let res = sigi("_integ::version", &["--version"]); 8 | res.assert_success(); 9 | res.assert_stdout_line_starts_with("sigi 3.7"); 10 | res.assert_stderr_empty(); 11 | } 12 | -------------------------------------------------------------------------------- /tests/empty_stack_tests.rs: -------------------------------------------------------------------------------- 1 | mod run_sigi; 2 | 3 | use run_sigi::sigi; 4 | 5 | #[test] 6 | fn sigi_empty_stack_ops() { 7 | let stack = "_integ::empty_stack"; 8 | 9 | let res = sigi(stack, &["delete-all"]); 10 | res.assert_success(); 11 | 12 | let res = sigi(stack, &[]); 13 | res.assert_success(); 14 | res.assert_stdout_eq("Now: NOTHING\n"); 15 | res.assert_stderr_empty(); 16 | 17 | let res = sigi(stack, &["peek"]); 18 | res.assert_success(); 19 | res.assert_stdout_eq("Now: NOTHING\n"); 20 | res.assert_stderr_empty(); 21 | 22 | let res = sigi(stack, &["list"]); 23 | res.assert_success(); 24 | res.assert_stdout_eq("Now: NOTHING\n"); 25 | res.assert_stderr_empty(); 26 | 27 | let res = sigi(stack, &["head"]); 28 | res.assert_success(); 29 | res.assert_stdout_eq("Now: NOTHING\n"); 30 | res.assert_stderr_empty(); 31 | 32 | let res = sigi(stack, &["tail"]); 33 | res.assert_success(); 34 | res.assert_stdout_eq("Now: NOTHING\n"); 35 | res.assert_stderr_empty(); 36 | 37 | let res = sigi(stack, &["count"]); 38 | res.assert_success(); 39 | res.assert_stdout_eq("0\n"); 40 | res.assert_stderr_empty(); 41 | 42 | let res = sigi(stack, &["is-empty"]); 43 | res.assert_success(); 44 | res.assert_stdout_eq("true\n"); 45 | res.assert_stderr_empty(); 46 | 47 | let res = sigi(stack, &["complete"]); 48 | res.assert_success(); 49 | res.assert_stdout_eq("Now: NOTHING\n"); 50 | res.assert_stderr_empty(); 51 | 52 | let res = sigi(stack, &["delete"]); 53 | res.assert_success(); 54 | res.assert_stdout_eq("Now: NOTHING\n"); 55 | res.assert_stderr_empty(); 56 | 57 | let res = sigi(stack, &["delete-all"]); 58 | res.assert_success(); 59 | res.assert_stdout_eq("Deleted: 0 items\n"); 60 | res.assert_stderr_empty(); 61 | 62 | let res = sigi(stack, &["list-stacks"]); 63 | res.assert_success(); 64 | res.assert_stdout_line_eq(stack); 65 | res.assert_stderr_empty(); 66 | } 67 | -------------------------------------------------------------------------------- /tests/interactive_tests.rs: -------------------------------------------------------------------------------- 1 | mod run_sigi; 2 | 3 | use run_sigi::{piping, sigi}; 4 | 5 | #[test] 6 | fn sigi_interactive_preamble() { 7 | let res = sigi("_integ::interactive", &["interactive"]); 8 | res.assert_success(); 9 | res.assert_stdout_line_starts_with("sigi 3.7"); 10 | res.assert_stdout_line_starts_with(r#"Type "exit", "quit", or "q" to quit"#); 11 | res.assert_stdout_line_starts_with( 12 | r#"Type "?" for quick help, or "help" for a more verbose help message"#, 13 | ); 14 | res.assert_stderr_empty(); 15 | } 16 | 17 | #[test] 18 | fn sigi_interactive_basic() { 19 | let res = piping(&["push hello world"]).into_sigi("_integ::interactive", &["interactive"]); 20 | res.assert_stdout_line_starts_with("sigi 3.7"); 21 | res.assert_stdout_line_starts_with(r#"Type "exit", "quit", or "q" to quit"#); 22 | res.assert_stdout_line_starts_with( 23 | r#"Type "?" for quick help, or "help" for a more verbose help message"#, 24 | ); 25 | res.assert_stdout_line_starts_with("Created: hello world"); 26 | res.assert_stdout_line_starts_with("Ctrl+d: Buen biåhe!"); 27 | res.assert_stderr_empty(); 28 | } 29 | 30 | #[test] 31 | fn sigi_interactive_basic_semicolons() { 32 | let res = piping(&["push goodbye; push hello; drop; drop"]) 33 | .into_sigi("_integ::interactive_semicolons", &["interactive"]); 34 | 35 | res.assert_stderr_empty(); 36 | res.assert_stdout_lines_eq(&[ 37 | "*", 38 | r#"Type "exit", "quit", or "q" to quit. (On Unixy systems, Ctrl+C or Ctrl+D also work)"#, 39 | r#"Type "?" for quick help, or "help" for a more verbose help message."#, 40 | "", 41 | "Created: goodbye", 42 | "Created: hello", 43 | "Deleted: hello", 44 | "Now: goodbye", 45 | "Deleted: goodbye", 46 | "Now: NOTHING", 47 | "Ctrl+d: Buen biåhe!", 48 | ]); 49 | } 50 | -------------------------------------------------------------------------------- /tests/run_sigi.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::process::{Command, Output, Stdio}; 3 | 4 | pub const SIGI_PATH: &str = std::env!("CARGO_BIN_EXE_sigi"); 5 | 6 | pub fn sigi(stack: &str, args: &[&str]) -> SigiOutput { 7 | return Command::new(SIGI_PATH) 8 | .arg("--stack") 9 | .arg(stack) 10 | .args(args) 11 | .output() 12 | .expect("Error running process") 13 | .into(); 14 | } 15 | 16 | pub fn piping(lines: &[&str]) -> SigiInput { 17 | SigiInput { 18 | stdin: lines.iter().map(|s| s.to_string()).collect(), 19 | } 20 | } 21 | 22 | pub struct SigiInput { 23 | stdin: Vec, 24 | } 25 | 26 | impl SigiInput { 27 | pub fn into_sigi(self, stack: &str, args: &[&str]) -> SigiOutput { 28 | let stdin = self.stdin.join("\n"); 29 | 30 | let process = Command::new(SIGI_PATH) 31 | .arg("--stack") 32 | .arg(stack) 33 | .args(args) 34 | .stdin(Stdio::piped()) 35 | .stdout(Stdio::piped()) 36 | .stderr(Stdio::piped()) 37 | .spawn() 38 | .expect("Error running process"); 39 | 40 | process 41 | .stdin 42 | .expect("Error sending stdin to sigi") 43 | .write_all(stdin.as_bytes()) 44 | .unwrap(); 45 | 46 | let mut stdout = String::new(); 47 | process.stdout.unwrap().read_to_string(&mut stdout).unwrap(); 48 | 49 | let mut stderr = String::new(); 50 | process.stderr.unwrap().read_to_string(&mut stderr).unwrap(); 51 | 52 | SigiOutput { 53 | status: SigiStatus::Unknown, 54 | stdout, 55 | stderr, 56 | } 57 | } 58 | } 59 | 60 | pub struct SigiOutput { 61 | status: SigiStatus, 62 | stdout: String, 63 | stderr: String, 64 | } 65 | 66 | #[derive(Debug, PartialEq)] 67 | enum SigiStatus { 68 | Success, 69 | Failure, 70 | Unknown, 71 | } 72 | 73 | impl From for SigiStatus { 74 | fn from(b: bool) -> Self { 75 | match b { 76 | true => SigiStatus::Success, 77 | false => SigiStatus::Failure, 78 | } 79 | } 80 | } 81 | 82 | impl SigiOutput { 83 | pub fn assert_success(&self) { 84 | assert_eq!(self.status, SigiStatus::Success); 85 | } 86 | 87 | pub fn assert_failure(&self) { 88 | assert_eq!(self.status, SigiStatus::Failure); 89 | } 90 | 91 | pub fn assert_stdout_eq(&self, expected_stdout: &str) { 92 | assert_eq!( 93 | &self.stdout, 94 | expected_stdout, 95 | "sigi stdout did not exactly match expectation.\n{}", 96 | self.stdout_for_errors() 97 | ); 98 | } 99 | 100 | pub fn assert_stdout_line_eq(&self, expected_line: &str) { 101 | let some_line_eqs = self.stdout.lines().any(|line| line == expected_line); 102 | assert!( 103 | some_line_eqs, 104 | "sigi stdout had no line matching: `{:?}`\n{}", 105 | expected_line, 106 | self.stdout_for_errors() 107 | ); 108 | } 109 | 110 | pub fn assert_stdout_lines_eq(&self, expected_lines: &[&str]) { 111 | self.stdout 112 | .lines() 113 | .zip(expected_lines.iter()) 114 | .enumerate() 115 | .for_each(|(i, (actual, expected))| { 116 | if *expected == "*" { 117 | return; 118 | } 119 | assert_eq!( 120 | &actual, 121 | expected, 122 | "sigi stdout did not match expected output `{}` on line {}\n{}", 123 | expected_lines.join("\n"), 124 | i, 125 | self.stdout_for_errors() 126 | ) 127 | }); 128 | } 129 | 130 | pub fn assert_stdout_line_starts_with(&self, expected_prefix: &str) { 131 | let some_line_eqs = self 132 | .stdout 133 | .lines() 134 | .any(|line| line.starts_with(expected_prefix)); 135 | assert!( 136 | some_line_eqs, 137 | "sigi stdout had no line starting with: {}\n{}", 138 | expected_prefix, 139 | self.stdout_for_errors() 140 | ); 141 | } 142 | 143 | pub fn assert_stderr_empty(&self) { 144 | assert_eq!( 145 | &self.stderr, 146 | "", 147 | "sigi stderr was expected to be empty.\n{}", 148 | self.stderr_for_errors() 149 | ); 150 | } 151 | 152 | fn stdout_for_errors(&self) -> String { 153 | format!( 154 | "===\nstdout:\n===\n{}\n===\n", 155 | self.stdout.lines().collect::>().join("\n") 156 | ) 157 | } 158 | 159 | fn stderr_for_errors(&self) -> String { 160 | format!( 161 | "===\nstderr:\n===\n{}\n===\n", 162 | self.stderr.lines().collect::>().join("\n") 163 | ) 164 | } 165 | } 166 | 167 | impl From for SigiOutput { 168 | fn from(output: Output) -> SigiOutput { 169 | SigiOutput { 170 | status: if output.status.success() { 171 | SigiStatus::Success 172 | } else { 173 | SigiStatus::Failure 174 | }, 175 | stdout: String::from_utf8(output.stdout).expect("Couldn't read stdout"), 176 | stderr: String::from_utf8(output.stderr).expect("Couldn't read stderr"), 177 | } 178 | } 179 | } 180 | 181 | #[test] 182 | fn assert_success() { 183 | let output = SigiOutput { 184 | status: true.into(), 185 | stdout: String::new(), 186 | stderr: String::new(), 187 | }; 188 | 189 | output.assert_success(); 190 | } 191 | 192 | #[test] 193 | fn assert_failure() { 194 | let output = SigiOutput { 195 | status: false.into(), 196 | stdout: String::new(), 197 | stderr: String::new(), 198 | }; 199 | 200 | output.assert_failure(); 201 | } 202 | 203 | #[test] 204 | fn assert_stdout_eq() { 205 | let output = SigiOutput { 206 | status: true.into(), 207 | stdout: "hello".to_string(), 208 | stderr: String::new(), 209 | }; 210 | 211 | output.assert_stdout_eq("hello"); 212 | } 213 | 214 | #[test] 215 | fn assert_stdout_line_eq() { 216 | let output = SigiOutput { 217 | status: true.into(), 218 | stdout: "hey\nhello".to_string(), 219 | stderr: String::new(), 220 | }; 221 | 222 | output.assert_stdout_line_eq("hello"); 223 | } 224 | 225 | #[test] 226 | fn assert_stdout_lines_eq() { 227 | let output = SigiOutput { 228 | status: true.into(), 229 | stdout: "hey\nhello there".to_string(), 230 | stderr: String::new(), 231 | }; 232 | 233 | output.assert_stdout_lines_eq(&["hey", "hello there"]); 234 | } 235 | 236 | #[test] 237 | fn assert_stdout_line_starts_with() { 238 | let output = SigiOutput { 239 | status: true.into(), 240 | stdout: "hey\nhello there".to_string(), 241 | stderr: String::new(), 242 | }; 243 | 244 | output.assert_stdout_line_starts_with("hello"); 245 | } 246 | 247 | #[test] 248 | fn assert_stderr_empty() { 249 | let output = SigiOutput { 250 | status: true.into(), 251 | stdout: "hey\nhello there".to_string(), 252 | stderr: String::new(), 253 | }; 254 | 255 | output.assert_stderr_empty(); 256 | } 257 | 258 | #[test] 259 | fn sigi_piping_basic() { 260 | let res = piping(&[]).into_sigi("_integ::basic", &["interactive"]); 261 | assert_eq!(res.status, SigiStatus::Unknown); 262 | res.assert_stdout_line_starts_with("sigi 3.7"); 263 | res.assert_stderr_empty(); 264 | } 265 | -------------------------------------------------------------------------------- /tests/single_item_tests.rs: -------------------------------------------------------------------------------- 1 | mod run_sigi; 2 | 3 | use run_sigi::sigi; 4 | 5 | #[test] 6 | fn sigi_single_item_ops() { 7 | let stack = "_integ::single_item"; 8 | 9 | let res = sigi(stack, &["delete-all"]); 10 | res.assert_success(); 11 | 12 | let res = sigi(stack, &["push", "hello"]); 13 | res.assert_success(); 14 | res.assert_stdout_eq("Created: hello\n"); 15 | res.assert_stderr_empty(); 16 | 17 | let res = sigi(stack, &[]); 18 | res.assert_success(); 19 | res.assert_stdout_eq("Now: hello\n"); 20 | res.assert_stderr_empty(); 21 | 22 | let res = sigi(stack, &["peek"]); 23 | res.assert_success(); 24 | res.assert_stdout_eq("Now: hello\n"); 25 | res.assert_stderr_empty(); 26 | 27 | let res = sigi(stack, &["list"]); 28 | res.assert_success(); 29 | res.assert_stdout_eq("Now: hello\n"); 30 | res.assert_stderr_empty(); 31 | 32 | let res = sigi(stack, &["head"]); 33 | res.assert_success(); 34 | res.assert_stdout_eq("Now: hello\n"); 35 | res.assert_stderr_empty(); 36 | 37 | let res = sigi(stack, &["tail"]); 38 | res.assert_success(); 39 | res.assert_stdout_eq("Now: hello\n"); 40 | res.assert_stderr_empty(); 41 | 42 | let res = sigi(stack, &["count"]); 43 | res.assert_success(); 44 | res.assert_stdout_eq("1\n"); 45 | res.assert_stderr_empty(); 46 | 47 | let res = sigi(stack, &["is-empty"]); 48 | res.assert_failure(); 49 | res.assert_stdout_eq("false\n"); 50 | res.assert_stderr_empty(); 51 | 52 | let res = sigi(stack, &["complete"]); 53 | res.assert_success(); 54 | res.assert_stdout_eq("Completed: hello\nNow: NOTHING\n"); 55 | res.assert_stderr_empty(); 56 | 57 | let res = sigi(stack, &[]); 58 | res.assert_success(); 59 | res.assert_stdout_eq("Now: NOTHING\n"); 60 | res.assert_stderr_empty(); 61 | 62 | let res = sigi(stack, &["count"]); 63 | res.assert_success(); 64 | res.assert_stdout_eq("0\n"); 65 | res.assert_stderr_empty(); 66 | 67 | let res = sigi(stack, &["is-empty"]); 68 | res.assert_success(); 69 | res.assert_stdout_eq("true\n"); 70 | res.assert_stderr_empty(); 71 | } 72 | --------------------------------------------------------------------------------