├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── pass-tmux └── pass-tmux.md ├── src ├── args.rs ├── lib.rs ├── main.rs ├── stats │ ├── mod.rs │ └── serialize.rs └── store │ ├── mod.rs │ └── serialize.rs └── tests ├── common └── mod.rs ├── integration ├── errors.rs ├── mod.rs ├── sort.rs └── weight.rs └── lib.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | test: 9 | name: test 10 | env: 11 | RUST_BACKTRACE: 1 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Install Rust 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | profile: minimal 22 | override: true 23 | 24 | - name: Build fre 25 | run: cargo build --verbose --all 26 | 27 | - name: Run tests 28 | run: cargo test --verbose --all 29 | 30 | rustfmt: 31 | name: rustfmt 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v2 36 | - name: Install Rust 37 | uses: actions-rs/toolchain@v1 38 | with: 39 | toolchain: stable 40 | override: true 41 | profile: minimal 42 | components: rustfmt 43 | - name: Check formatting 44 | run: | 45 | cargo fmt -- --check 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # The way this works is a little weird. But basically, the create-release job 2 | # runs purely to initialize the GitHub release itself. Once done, the upload 3 | # URL of the release is saved as an artifact. 4 | # 5 | # The build-release job runs only once create-release is finished. It gets 6 | # the release upload URL by downloading the corresponding artifact (which was 7 | # uploaded by create-release). It then builds the release executables for each 8 | # supported platform and attaches them as release assets to the previously 9 | # created release. 10 | # 11 | # The key here is that we create the release only once. 12 | 13 | name: release 14 | on: 15 | push: 16 | # Enable when testing release infrastructure on a branch. 17 | # branches: 18 | # - add-ci 19 | tags: 20 | - '[0-9]+.[0-9]+.[0-9]+' 21 | jobs: 22 | create-release: 23 | name: create-release 24 | runs-on: ubuntu-latest 25 | # env: 26 | # Set to force version number, e.g., when no tag exists. 27 | # FRE_VERSION: TEST-0.0.0 28 | steps: 29 | - name: Create artifacts directory 30 | run: mkdir artifacts 31 | 32 | - name: Get the release version from the tag 33 | if: env.FRE_VERSION == '' 34 | run: | 35 | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 36 | echo "FRE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 37 | echo "version is: ${{ env.FRE_VERSION }}" 38 | 39 | - name: Create GitHub release 40 | id: release 41 | uses: actions/create-release@v1 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | with: 45 | tag_name: ${{ env.FRE_VERSION }} 46 | release_name: ${{ env.FRE_VERSION }} 47 | 48 | - name: Save release upload URL to artifact 49 | run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url 50 | 51 | - name: Save version number to artifact 52 | run: echo "${{ env.FRE_VERSION }}" > artifacts/release-version 53 | 54 | - name: Upload artifacts 55 | uses: actions/upload-artifact@v1 56 | with: 57 | name: artifacts 58 | path: artifacts 59 | 60 | build-release: 61 | name: build-release 62 | needs: ['create-release'] 63 | runs-on: ${{ matrix.os }} 64 | env: 65 | TARGET_DIR: ./target 66 | RUST_BACKTRACE: 1 67 | strategy: 68 | matrix: 69 | build: [linux-musl, linux-gnu, macos] 70 | include: 71 | - build: linux-musl 72 | os: ubuntu-latest 73 | rust: nightly 74 | target: x86_64-unknown-linux-musl 75 | - build: linux-gnu 76 | os: ubuntu-latest 77 | rust: nightly 78 | target: x86_64-unknown-linux-gnu 79 | - build: macos 80 | os: macos-latest 81 | rust: nightly 82 | target: x86_64-apple-darwin 83 | 84 | steps: 85 | - name: Checkout repository 86 | uses: actions/checkout@v2 87 | with: 88 | fetch-depth: 1 89 | 90 | - name: Install Rust 91 | uses: actions-rs/toolchain@v1 92 | with: 93 | toolchain: ${{ matrix.rust }} 94 | profile: minimal 95 | override: true 96 | target: ${{ matrix.target }} 97 | 98 | - name: Use set target flags 99 | run: | 100 | echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV 101 | echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV 102 | 103 | - name: Show command used for Cargo 104 | run: | 105 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 106 | echo "target dir is: ${{ env.TARGET_DIR }}" 107 | 108 | - name: Get release download URL 109 | uses: actions/download-artifact@v1 110 | with: 111 | name: artifacts 112 | path: artifacts 113 | 114 | - name: Set release upload URL and release version 115 | shell: bash 116 | run: | 117 | release_upload_url="$(cat artifacts/release-upload-url)" 118 | echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV 119 | echo "release upload url: $RELEASE_UPLOAD_URL" 120 | release_version="$(cat artifacts/release-version)" 121 | echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV 122 | echo "release version: $RELEASE_VERSION" 123 | 124 | - name: Build release binary 125 | run: cargo build --verbose --release ${{ env.TARGET_FLAGS }} 126 | 127 | - name: Strip release binary 128 | run: strip "target/${{ matrix.target }}/release/fre" 129 | 130 | - name: Upload release archive 131 | uses: actions/upload-release-asset@v1.0.1 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | with: 135 | upload_url: ${{ env.RELEASE_UPLOAD_URL }} 136 | asset_path: target/${{ matrix.target }}/release/fre 137 | asset_name: fre-${{ matrix.target }} 138 | asset_content_type: application/octet-stream 139 | 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .idea 4 | .project 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 0.4.0 - 2023-12-28 9 | 10 | ### Added 11 | 12 | - New `--stat-digits` to specify the number of digits of precision for the `--stat` format 13 | 14 | ### Mainenance 15 | 16 | - Updated and removed various dependencies 17 | 18 | ## 0.3.1 - 2020-12-08 19 | 20 | ### Added 21 | 22 | - New `--delete` option to remove an item from the store 23 | - Example of purging directires that no longer exist from the store 24 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.4" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.4" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.2" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 48 | dependencies = [ 49 | "windows-sys 0.48.0", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "3.0.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys 0.48.0", 60 | ] 61 | 62 | [[package]] 63 | name = "anyhow" 64 | version = "1.0.75" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 67 | 68 | [[package]] 69 | name = "assert_cmd" 70 | version = "0.10.2" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "b7ac5c260f75e4e4ba87b7342be6edcecbcb3eb6741a0507fda7ad115845cc65" 73 | dependencies = [ 74 | "escargot", 75 | "predicates", 76 | "predicates-core", 77 | "predicates-tree", 78 | ] 79 | 80 | [[package]] 81 | name = "autocfg" 82 | version = "1.1.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 85 | 86 | [[package]] 87 | name = "bitflags" 88 | version = "1.3.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 91 | 92 | [[package]] 93 | name = "bitflags" 94 | version = "2.4.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 97 | 98 | [[package]] 99 | name = "cfg-if" 100 | version = "1.0.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 103 | 104 | [[package]] 105 | name = "clap" 106 | version = "4.4.10" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" 109 | dependencies = [ 110 | "clap_builder", 111 | "clap_derive", 112 | ] 113 | 114 | [[package]] 115 | name = "clap_builder" 116 | version = "4.4.9" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" 119 | dependencies = [ 120 | "anstream", 121 | "anstyle", 122 | "clap_lex", 123 | "strsim", 124 | ] 125 | 126 | [[package]] 127 | name = "clap_derive" 128 | version = "4.4.7" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 131 | dependencies = [ 132 | "heck", 133 | "proc-macro2", 134 | "quote", 135 | "syn", 136 | ] 137 | 138 | [[package]] 139 | name = "clap_lex" 140 | version = "0.6.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 143 | 144 | [[package]] 145 | name = "colorchoice" 146 | version = "1.0.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 149 | 150 | [[package]] 151 | name = "difference" 152 | version = "2.0.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 155 | 156 | [[package]] 157 | name = "directories" 158 | version = "1.0.2" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "72d337a64190607d4fcca2cb78982c5dd57f4916e19696b48a575fa746b6cb0f" 161 | dependencies = [ 162 | "libc", 163 | "winapi", 164 | ] 165 | 166 | [[package]] 167 | name = "errno" 168 | version = "0.3.8" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 171 | dependencies = [ 172 | "libc", 173 | "windows-sys 0.52.0", 174 | ] 175 | 176 | [[package]] 177 | name = "escargot" 178 | version = "0.3.1" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "19db1f7e74438642a5018cdf263bb1325b2e792f02dd0a3ca6d6c0f0d7b1d5a5" 181 | dependencies = [ 182 | "serde", 183 | "serde_json", 184 | ] 185 | 186 | [[package]] 187 | name = "fastrand" 188 | version = "2.0.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 191 | 192 | [[package]] 193 | name = "float-cmp" 194 | version = "0.8.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" 197 | dependencies = [ 198 | "num-traits", 199 | ] 200 | 201 | [[package]] 202 | name = "fre" 203 | version = "0.4.1" 204 | dependencies = [ 205 | "anyhow", 206 | "assert_cmd", 207 | "clap", 208 | "directories", 209 | "predicates", 210 | "serde", 211 | "serde_derive", 212 | "serde_json", 213 | "tempfile", 214 | ] 215 | 216 | [[package]] 217 | name = "heck" 218 | version = "0.4.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 221 | 222 | [[package]] 223 | name = "itoa" 224 | version = "1.0.9" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 227 | 228 | [[package]] 229 | name = "libc" 230 | version = "0.2.150" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 233 | 234 | [[package]] 235 | name = "linux-raw-sys" 236 | version = "0.4.12" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" 239 | 240 | [[package]] 241 | name = "memchr" 242 | version = "2.6.4" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 245 | 246 | [[package]] 247 | name = "normalize-line-endings" 248 | version = "0.3.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 251 | 252 | [[package]] 253 | name = "num-traits" 254 | version = "0.2.17" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 257 | dependencies = [ 258 | "autocfg", 259 | ] 260 | 261 | [[package]] 262 | name = "predicates" 263 | version = "1.0.8" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df" 266 | dependencies = [ 267 | "difference", 268 | "float-cmp", 269 | "normalize-line-endings", 270 | "predicates-core", 271 | "regex", 272 | ] 273 | 274 | [[package]] 275 | name = "predicates-core" 276 | version = "1.0.6" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 279 | 280 | [[package]] 281 | name = "predicates-tree" 282 | version = "1.0.9" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 285 | dependencies = [ 286 | "predicates-core", 287 | "termtree", 288 | ] 289 | 290 | [[package]] 291 | name = "proc-macro2" 292 | version = "1.0.70" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 295 | dependencies = [ 296 | "unicode-ident", 297 | ] 298 | 299 | [[package]] 300 | name = "quote" 301 | version = "1.0.33" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 304 | dependencies = [ 305 | "proc-macro2", 306 | ] 307 | 308 | [[package]] 309 | name = "redox_syscall" 310 | version = "0.4.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 313 | dependencies = [ 314 | "bitflags 1.3.2", 315 | ] 316 | 317 | [[package]] 318 | name = "regex" 319 | version = "1.10.2" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 322 | dependencies = [ 323 | "aho-corasick", 324 | "memchr", 325 | "regex-automata", 326 | "regex-syntax", 327 | ] 328 | 329 | [[package]] 330 | name = "regex-automata" 331 | version = "0.4.3" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 334 | dependencies = [ 335 | "aho-corasick", 336 | "memchr", 337 | "regex-syntax", 338 | ] 339 | 340 | [[package]] 341 | name = "regex-syntax" 342 | version = "0.8.2" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 345 | 346 | [[package]] 347 | name = "rustix" 348 | version = "0.38.26" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" 351 | dependencies = [ 352 | "bitflags 2.4.1", 353 | "errno", 354 | "libc", 355 | "linux-raw-sys", 356 | "windows-sys 0.52.0", 357 | ] 358 | 359 | [[package]] 360 | name = "ryu" 361 | version = "1.0.15" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 364 | 365 | [[package]] 366 | name = "serde" 367 | version = "1.0.193" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 370 | dependencies = [ 371 | "serde_derive", 372 | ] 373 | 374 | [[package]] 375 | name = "serde_derive" 376 | version = "1.0.193" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 379 | dependencies = [ 380 | "proc-macro2", 381 | "quote", 382 | "syn", 383 | ] 384 | 385 | [[package]] 386 | name = "serde_json" 387 | version = "1.0.108" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 390 | dependencies = [ 391 | "itoa", 392 | "ryu", 393 | "serde", 394 | ] 395 | 396 | [[package]] 397 | name = "strsim" 398 | version = "0.10.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 401 | 402 | [[package]] 403 | name = "syn" 404 | version = "2.0.39" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 407 | dependencies = [ 408 | "proc-macro2", 409 | "quote", 410 | "unicode-ident", 411 | ] 412 | 413 | [[package]] 414 | name = "tempfile" 415 | version = "3.8.1" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" 418 | dependencies = [ 419 | "cfg-if", 420 | "fastrand", 421 | "redox_syscall", 422 | "rustix", 423 | "windows-sys 0.48.0", 424 | ] 425 | 426 | [[package]] 427 | name = "termtree" 428 | version = "0.4.1" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 431 | 432 | [[package]] 433 | name = "unicode-ident" 434 | version = "1.0.12" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 437 | 438 | [[package]] 439 | name = "utf8parse" 440 | version = "0.2.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 443 | 444 | [[package]] 445 | name = "winapi" 446 | version = "0.3.9" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 449 | dependencies = [ 450 | "winapi-i686-pc-windows-gnu", 451 | "winapi-x86_64-pc-windows-gnu", 452 | ] 453 | 454 | [[package]] 455 | name = "winapi-i686-pc-windows-gnu" 456 | version = "0.4.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 459 | 460 | [[package]] 461 | name = "winapi-x86_64-pc-windows-gnu" 462 | version = "0.4.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 465 | 466 | [[package]] 467 | name = "windows-sys" 468 | version = "0.48.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 471 | dependencies = [ 472 | "windows-targets 0.48.5", 473 | ] 474 | 475 | [[package]] 476 | name = "windows-sys" 477 | version = "0.52.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 480 | dependencies = [ 481 | "windows-targets 0.52.0", 482 | ] 483 | 484 | [[package]] 485 | name = "windows-targets" 486 | version = "0.48.5" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 489 | dependencies = [ 490 | "windows_aarch64_gnullvm 0.48.5", 491 | "windows_aarch64_msvc 0.48.5", 492 | "windows_i686_gnu 0.48.5", 493 | "windows_i686_msvc 0.48.5", 494 | "windows_x86_64_gnu 0.48.5", 495 | "windows_x86_64_gnullvm 0.48.5", 496 | "windows_x86_64_msvc 0.48.5", 497 | ] 498 | 499 | [[package]] 500 | name = "windows-targets" 501 | version = "0.52.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 504 | dependencies = [ 505 | "windows_aarch64_gnullvm 0.52.0", 506 | "windows_aarch64_msvc 0.52.0", 507 | "windows_i686_gnu 0.52.0", 508 | "windows_i686_msvc 0.52.0", 509 | "windows_x86_64_gnu 0.52.0", 510 | "windows_x86_64_gnullvm 0.52.0", 511 | "windows_x86_64_msvc 0.52.0", 512 | ] 513 | 514 | [[package]] 515 | name = "windows_aarch64_gnullvm" 516 | version = "0.48.5" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 519 | 520 | [[package]] 521 | name = "windows_aarch64_gnullvm" 522 | version = "0.52.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 525 | 526 | [[package]] 527 | name = "windows_aarch64_msvc" 528 | version = "0.48.5" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 531 | 532 | [[package]] 533 | name = "windows_aarch64_msvc" 534 | version = "0.52.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 537 | 538 | [[package]] 539 | name = "windows_i686_gnu" 540 | version = "0.48.5" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 543 | 544 | [[package]] 545 | name = "windows_i686_gnu" 546 | version = "0.52.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 549 | 550 | [[package]] 551 | name = "windows_i686_msvc" 552 | version = "0.48.5" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 555 | 556 | [[package]] 557 | name = "windows_i686_msvc" 558 | version = "0.52.0" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 561 | 562 | [[package]] 563 | name = "windows_x86_64_gnu" 564 | version = "0.48.5" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 567 | 568 | [[package]] 569 | name = "windows_x86_64_gnu" 570 | version = "0.52.0" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 573 | 574 | [[package]] 575 | name = "windows_x86_64_gnullvm" 576 | version = "0.48.5" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 579 | 580 | [[package]] 581 | name = "windows_x86_64_gnullvm" 582 | version = "0.52.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 585 | 586 | [[package]] 587 | name = "windows_x86_64_msvc" 588 | version = "0.48.5" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 591 | 592 | [[package]] 593 | name = "windows_x86_64_msvc" 594 | version = "0.52.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 597 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fre" 3 | version = "0.4.1" 4 | authors = ["Camden Cheek "] 5 | description = "A command line frecency tracking tool" 6 | edition = '2021' 7 | license = 'MIT' 8 | 9 | [dependencies] 10 | clap = {version = "4.4", features = ["derive"]} 11 | serde = {version = "1.0.75", features = ["rc"]} 12 | serde_derive = "1.0.75" 13 | serde_json = "1.0.26" 14 | directories = "1.0.2" 15 | tempfile = "3.0.3" 16 | anyhow = "1.0.75" 17 | 18 | [dev-dependencies] 19 | assert_cmd = "0.10" 20 | predicates = "1.0" 21 | 22 | [profile.release] 23 | lto = true 24 | opt-level = "s" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Camden Cheek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FREcency tracking (`fre`) 2 | 3 | `fre` is a CLI tool for tracking your most-used directories and files. 4 | Though inspired by tools like `autojump` or the `z` plugin for `zsh`, it takes a slightly 5 | different approach to tracking and providing usage data. 6 | The primary difference is `fre` does not support jumping. Instead, 7 | it just keeps track of and provides sorting methods for directories, 8 | which can then be filtered by another application like `fzf`, 9 | which does a much better job of filtering than something I can write. 10 | Additionally, it uses an algorithm in which the weights of each directory 11 | decay exponentially, so more recently used directories are ranked more highly 12 | in a smooth manner. 13 | 14 | 15 | ## Usage 16 | 17 | `fre` is primarily designed to interface with `fzf`. For general usage, 18 | a user will create a shell hook that adds a directory every time the current 19 | directory is changed. This will start to build your profile of most-used directories. 20 | Then, `fre` can be used as a source for `fzf`. I personally use the `fzf`-provided 21 | control-T bindings, modified to use `fre` as input. Some examples are below. 22 | 23 | Basic usage 24 | ```sh 25 | # Print directories, sorted by frecency, then pipe to fzf 26 | fre --sorted | fzf --no-sort 27 | 28 | # Print directories and their associated frecency, sorted by frecency 29 | fre --stat 30 | 31 | # Log a visit to a directory 32 | fre --add /home/user/new_dir 33 | 34 | # Decrease weight of a directory by 10 visits 35 | fre --decrease 10 /home/user/too_high_dir 36 | 37 | # Print directories and the time since they were last visited in hours 38 | fre --stat --sort_method recent 39 | 40 | # Print directories and the number of times they've been visited 41 | fre --stat --sort_method frequent 42 | 43 | # Purge directories that no longer exist 44 | fre --sorted | while read dir ; do if [ ! -d "$dir" ] ; then fre --delete "$dir"; fi ; done 45 | ``` 46 | 47 | ## Installation 48 | 49 | From source: `git clone https://github.com/camdencheek/fre.git && cargo install --path ./fre` 50 | 51 | From crate: `cargo install fre` 52 | 53 | Arch linux: `yay -S fre` 54 | 55 | macOS: `brew install camdencheek/brew/fre` 56 | 57 | For integration with `fzf` CTRL-T, define the following environment variables 58 | ```zsh 59 | export FZF_CTRL_T_COMMAND='command fre --sorted' 60 | export FZF_CTRL_T_OPTS='--tiebreak=index' 61 | ``` 62 | 63 | To preferentially use results from `fre`, but fall back to other results, we can use 64 | `cat` to combine results before sending them to `fzf`. My favorite alternate source 65 | is `fd` ([link](https://github.com/sharkdp/fd)), but the more common `find` can also be 66 | used. The following options first use `fre` results, then use all the subdirectories 67 | of the current directory, then use every subdirectory in your home directory. 68 | This is what I personally use. 69 | 70 | ```zsh 71 | export FZF_CTRL_T_COMMAND='command cat <(fre --sorted) <(fd -t d) <(fd -t d . ~)' 72 | export FZF_CTRL_T_OPTS='--tiebreak=index' 73 | ``` 74 | 75 | ### Shell integration 76 | 77 | Don't see your shell here? feel free to open a PR to add it! 78 | 79 | #### zsh 80 | (credit to `autojump`) 81 | 82 | ```zsh 83 | fre_chpwd() { 84 | fre --add "$(pwd)" 85 | } 86 | typeset -gaU chpwd_functions 87 | chpwd_functions+=fre_chpwd 88 | ``` 89 | 90 | #### bash 91 | (credit to `autojump`) 92 | 93 | In your `~/.profile`: 94 | 95 | ```zsh 96 | PROMPT_COMMAND="${PROMPT_COMMAND:+$(echo "${PROMPT_COMMAND}" | awk '{gsub(/; *$/,"")}2') ; }"'fre --add "$(pwd)"' 97 | ``` 98 | 99 | ## Comparison to existing solutions 100 | 101 | The three projects I'm familiar with that are closest in function to this are `autojump`, the `z` shell plugin, and the `d` portion (and maybe the `f` in the future) of `fasd`. 102 | 103 | The primary difference from the rest of these is its reliance on a tool like `fzf` to provide any solid directory jumping functionality. This was an intentional choice, sticking to the Unix philosophy of "do one thing, and do it well". 104 | 105 | The other major change from these pieces of software is the algorithm used to rank directories. `autojump` uses the following formula: 106 | 107 | ```python 108 | 109 | def add_path(data, path, weight=10): 110 | # ... 111 | data[path] = sqrt((data.get(path, 0) ** 2) + (weight ** 2)) 112 | # ... 113 | ``` 114 | 115 | Looking at it closely, it seems to just be calculating the hypotenuse of a triangle where one side is the length of the previous weight and the other is the length of the weight being added. This does not take into account time passed since access at all, which is not ideal since I would rather not have directories from years ago ranked highly. 116 | 117 | `fasd` and `z` both use the same frecency function that looks something like this: 118 | 119 | ```zsh 120 | function frecent(rank, time) { 121 | dx = t-time 122 | if( dx < 3600 ) return rank*4 123 | if( dx < 86400 ) return rank*2 124 | if( dx < 604800 ) return rank/2 125 | return rank/4 126 | } 127 | ``` 128 | 129 | This works fine until you re-visit an old directory. Then, suddenly, `dx` is small again and all the old visits are re-weighted to `rank*4`, causing it to jump up in the sorted output. This is not really ideal. I want to be able to re-visit an old directory once without messing up my directory ranking. 130 | 131 | `fre` uses a frecency algorithm where the weight of a directory visit decays over time. Given a list of visit times (bold x), the frecency of the directory would look something like this (using lambda as the half life and "now" as the current time at calculation): 132 | 133 | 134 | 135 | With a little bit of mathemagics, we don't actually have to store the vector of access times. We can compress everything down into one number as long as we're okay not being able to dynamically change the half life. 136 | 137 | This algorithm provides a much more intuitive implementation of frecency that tends to come up with results that more closely match those we would naturally expect. 138 | 139 | ## Support 140 | 141 | I use this regularly on MacOS and Linux. I wrote it to be usable on Windows as well, 142 | but I don't run any tests for it. Caveat emptor. 143 | 144 | 145 | ## Stability 146 | 147 | I've been using this for over a year with no changes now, and it does everything I need it to do. I'm happy to add features or accept changes if this is not the case for you. 148 | 149 | ## About the algorithm 150 | 151 | The algorithm used combines the concepts of frequency and recency into a single, sortable statistic called "frecency". 152 | To my knowledge, this term was first coined by Mozilla to describe their URL suggestions algorithm. 153 | In fact, Mozilla already came up with nearly this exact algorithm and 154 | [considered using it to replace Firefox's frecency algorithm](https://wiki.mozilla.org/User:Jesse/NewFrecency?title=User:Jesse/NewFrecency). 155 | The algorithm is also very similar to the cache replacement problem, and a more formal treatment of the 156 | math behind it can be found in this [IEEE article](https://ieeexplore.ieee.org/document/970573) (sorry for the paywall). 157 | 158 | -------------------------------------------------------------------------------- /examples/pass-tmux: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -u 4 | 5 | FRE=$(command -v fre) 6 | 7 | # use fre for persisting choices 8 | if [ -x "${FRE}" ]; then 9 | XDG_STATE_HOME=${XDG_STATE_HOME:-${HOME}/.local/state} 10 | STORE_NAME="${0##*/}.json" 11 | else 12 | FRE=":" 13 | STORE_NAME="" 14 | fi 15 | 16 | EXT="gpg" 17 | NAME=$( 18 | { 19 | ${FRE} --store_name="${STORE_NAME}" --sorted 20 | pass git ls-files "*.${EXT}" | sed -e "s/\.${EXT}$//" 21 | } | awk '!x[$0]++' | fzf-tmux 22 | ) 23 | 24 | if [ -z "${NAME}" ]; then 25 | exit 26 | fi 27 | 28 | # save selection 29 | ${FRE} --store_name="${STORE_NAME}" -a "${NAME}" 30 | 31 | if [ "${1:-}" = "--paste" ]; then 32 | PASSWORD=$(pass show "${NAME}" | head -n1) 33 | tmux send-keys "${PASSWORD}" 34 | else 35 | pass show -c "${NAME}" > /dev/null 36 | fi 37 | -------------------------------------------------------------------------------- /examples/pass-tmux.md: -------------------------------------------------------------------------------- 1 | Integration of `tmux` with `pass`, using `fre` to show frecent selections first via `fzf`. 2 | 3 | Inspired by [`passmux`](https://github.com/hughdavenport/passmux/) 4 | 5 | # Prerequisites 6 | 7 | - [pass](https://www.passwordstore.org/) 8 | - [fzf](https://github.com/junegunn/fzf/) 9 | - [tmux](https://github.com/tmux/tmux/) 10 | 11 | # Installation 12 | 13 | 1. Copy `pass-tmux` to `~/.local/bin` (or somewhere in your path) 14 | 2. In `tmux.conf`, add the following key bindings: 15 | 16 | ``` 17 | bind-key -T prefix C-p run-shell -b 'pass-tmux' 18 | bind-key -T prefix C-t run-shell -b 'pass-tmux --paste' 19 | ``` 20 | 21 | Note: the `--paste` option inserts the retrieved password into the active pane (without sending Enter). 22 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use clap::{builder::OsStr, Args, Parser, ValueEnum}; 3 | use directories::ProjectDirs; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(name = env!("CARGO_PKG_NAME"))] 8 | #[command(author = env!("CARGO_PKG_AUTHORS"))] 9 | #[command(version = env!("CARGO_PKG_VERSION"))] 10 | pub struct Cli { 11 | /// Use a non-default store file 12 | #[arg(long = "store_name", conflicts_with = "store")] 13 | pub store_name: Option, 14 | 15 | /// Use a non-default filename for the store file in the default store directory 16 | #[arg(long, conflicts_with = "store_name")] 17 | pub store: Option, 18 | 19 | #[command(flatten)] 20 | pub updates: UpdateArgs, 21 | 22 | #[command(flatten)] 23 | pub stats: StatsArgs, 24 | 25 | /// The method to sort output by 26 | #[arg(long="sort_method", value_enum, default_value = SortMethod::Frecent)] 27 | pub sort_method: SortMethod, 28 | 29 | #[command(flatten)] 30 | pub janitor: JanitorArgs, 31 | 32 | /// The item to update 33 | pub item: Option, 34 | } 35 | 36 | #[derive(Args, Debug)] 37 | #[group(multiple = false, conflicts_with = "StatsArgs")] 38 | pub struct UpdateArgs { 39 | /// Add a visit to ITEM to the store 40 | #[arg(short = 'a', long)] 41 | pub add: bool, 42 | 43 | /// Increase the weight of an item by WEIGHT 44 | #[arg(short = 'i', long, value_name = "WEIGHT")] 45 | pub increase: Option, 46 | 47 | /// Delete ITEM from the store 48 | #[arg(short = 'D', long)] 49 | pub delete: bool, 50 | 51 | /// Decrease the weight of a path by WEIGHT 52 | #[arg(short = 'd', long)] 53 | pub decrease: Option, 54 | } 55 | 56 | #[derive(Args, Debug)] 57 | pub struct StatsArgs { 58 | /// Print the stored directories in order of highest to lowest score 59 | #[arg(long, group = "list")] 60 | pub sorted: bool, 61 | 62 | /// Print statistics about the stored directories 63 | #[arg(long, group = "list")] 64 | pub stat: bool, 65 | 66 | /// Limit the number of results printed with --sorted or --stat 67 | #[arg(long, requires = "list")] 68 | pub limit: Option, 69 | 70 | /// Override the number of digits shown with --stat 71 | #[arg(long, requires = "stat")] 72 | pub stat_digits: Option, 73 | } 74 | 75 | #[derive(Debug, Clone, Copy, ValueEnum)] 76 | pub enum SortMethod { 77 | Recent, 78 | Frequent, 79 | Frecent, 80 | } 81 | 82 | impl From for OsStr { 83 | fn from(value: SortMethod) -> Self { 84 | match value { 85 | SortMethod::Recent => OsStr::from("recent"), 86 | SortMethod::Frequent => OsStr::from("frequent"), 87 | SortMethod::Frecent => OsStr::from("frecent"), 88 | } 89 | } 90 | } 91 | 92 | #[derive(Args, Debug)] 93 | pub struct JanitorArgs { 94 | /// Change the halflife to N seconds (default 3 days) 95 | #[arg(long, value_name = "N")] 96 | pub halflife: Option, 97 | 98 | /// Truncate the stored items to only the top N 99 | #[arg(long, short = 'T', value_name = "N")] 100 | pub truncate: Option, 101 | } 102 | 103 | /// Given the argument matches, return the path of the store file. 104 | pub fn get_store_path(args: &Cli) -> Result { 105 | match (&args.store, &args.store_name) { 106 | (Some(dir), None) => Ok(dir.to_owned()), 107 | (None, filename) => default_store(filename.to_owned()), 108 | _ => unreachable!(), 109 | } 110 | } 111 | 112 | /// Return a path to a store file in the default location. 113 | /// Uses filename as the name of the file if it is not `None`. 114 | pub fn default_store(filename: Option) -> Result { 115 | let store_dir = match ProjectDirs::from("", "", env!("CARGO_PKG_NAME")) { 116 | Some(dir) => dir.data_dir().to_path_buf(), 117 | None => return Err(anyhow!("failed to determine default store directory")), 118 | }; 119 | 120 | let filename = 121 | filename.unwrap_or_else(|| PathBuf::from(format!("{}.json", env!("CARGO_PKG_NAME")))); 122 | let mut store_file = store_dir; 123 | store_file.push(filename); 124 | 125 | Ok(store_file.to_path_buf()) 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | 132 | #[test] 133 | fn get_store_path_full() { 134 | let arg_vec = vec!["fre", "--store", "/test/path"]; 135 | let args = Cli::try_parse_from(arg_vec).unwrap(); 136 | 137 | let store_path = get_store_path(&args).unwrap(); 138 | 139 | assert_eq!(PathBuf::from("/test/path"), store_path); 140 | } 141 | 142 | #[test] 143 | fn get_store_path_file() { 144 | let arg_vec = vec!["fre", "--store_name", "test.path"]; 145 | let args = Cli::try_parse_from(arg_vec).unwrap(); 146 | 147 | let store_path = get_store_path(&args).unwrap(); 148 | 149 | assert_eq!( 150 | store_path 151 | .file_name() 152 | .expect("no filename on store path") 153 | .to_os_string() 154 | .to_string_lossy(), 155 | "test.path".to_string() 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | use std::time::SystemTime; 5 | 6 | pub mod args; 7 | pub mod stats; 8 | pub mod store; 9 | 10 | /// Return the current time in seconds as a float 11 | pub fn current_time_secs() -> f64 { 12 | SystemTime::now() 13 | .duration_since(SystemTime::UNIX_EPOCH) 14 | .expect("failed to get system time") 15 | .as_secs_f64() 16 | } 17 | 18 | #[macro_export] 19 | macro_rules! error_and_exit { 20 | ($($arg:tt)*) => { 21 | { 22 | error!($($arg)*); 23 | ::std::process::exit(1); 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::{stdout, BufWriter}; 2 | 3 | use anyhow::{Context, Result}; 4 | use clap::Parser; 5 | use fre::{args::Cli, store::write_stats, *}; 6 | 7 | fn main() -> Result<()> { 8 | let args = Cli::try_parse()?; 9 | 10 | // Construct the path to the store file 11 | let store_file = args::get_store_path(&args)?; 12 | 13 | // Attempt to read and unmarshal the store file 14 | let mut usage = store::read_store(&store_file) 15 | .with_context(|| format!("failed to read store file {:?}", &store_file))?; 16 | 17 | // If a new half life is defined, parse and set it 18 | if let Some(h) = args.janitor.halflife { 19 | usage.set_half_life(h); 20 | } 21 | 22 | // TODO write a test for this 23 | if usage.half_lives_passed() > 5.0 { 24 | usage.reset_time() 25 | } 26 | 27 | // Print the directories if --sorted or --stat are specified 28 | if args.stats.sorted || args.stats.stat { 29 | let sorted = usage.sorted(args.sort_method); 30 | let mut sorted = sorted.as_slice(); 31 | if let Some(l) = args.stats.limit { 32 | sorted = &sorted[..usize::min(sorted.len(), l)] 33 | } 34 | 35 | let stdout = stdout(); 36 | let handle = stdout.lock(); 37 | let mut w = BufWriter::new(handle); 38 | write_stats( 39 | &mut w, 40 | sorted, 41 | args.sort_method, 42 | args.stats.stat, 43 | current_time_secs(), 44 | args.stats.stat_digits, 45 | )?; 46 | } 47 | 48 | // Increment a directory 49 | if args.updates.add { 50 | usage.add(args.item.as_ref().expect("add requires an item")); 51 | } 52 | 53 | // Handle increasing or decreasing a directory's score by a given weight 54 | if args.updates.increase.is_some() || args.updates.decrease.is_some() { 55 | // Get a positive weight if increase, negative if decrease 56 | let weight = match (args.updates.increase, args.updates.decrease) { 57 | (Some(i), None) => i, 58 | (None, Some(d)) => -d, 59 | _ => panic!("increase and decrease cannot both be set"), // enforced by clap and block guard 60 | }; 61 | 62 | usage.adjust( 63 | args.item 64 | .as_ref() 65 | .expect("item is required for increase or decrease"), 66 | weight, 67 | ); 68 | } 69 | 70 | // Delete a directory 71 | if args.updates.delete { 72 | usage.delete(&args.item.expect("delete requires an item")); 73 | } 74 | 75 | // Truncate store to top N directories 76 | if let Some(n) = args.janitor.truncate { 77 | usage.truncate(n, args.sort_method); 78 | } 79 | 80 | // Write the updated store file 81 | store::write_store(usage, &store_file).context("writing store")?; 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /src/stats/mod.rs: -------------------------------------------------------------------------------- 1 | use super::current_time_secs; 2 | use crate::args::SortMethod; 3 | use std::cmp::Ordering; 4 | 5 | pub mod serialize; 6 | 7 | /// A representation of statistics for a single item 8 | #[derive(Clone)] 9 | pub struct ItemStats { 10 | pub item: String, 11 | half_life: f64, 12 | // Time in seconds since the epoch 13 | reference_time: f64, 14 | // Time in seconds since reference_time that this item was last accessed 15 | last_accessed: f64, 16 | frecency: f64, 17 | pub num_accesses: i32, 18 | } 19 | 20 | impl ItemStats { 21 | /// Create a new item 22 | pub fn new(item: String, ref_time: f64, half_life: f64) -> ItemStats { 23 | ItemStats { 24 | half_life, 25 | reference_time: ref_time, 26 | item, 27 | frecency: 0.0, 28 | last_accessed: 0.0, 29 | num_accesses: 0, 30 | } 31 | } 32 | 33 | /// Compare the score of two items given a sort method 34 | pub fn cmp_score(&self, other: &ItemStats, method: SortMethod) -> Ordering { 35 | match method { 36 | SortMethod::Frequent => self.cmp_frequent(other), 37 | SortMethod::Recent => self.cmp_recent(other), 38 | SortMethod::Frecent => self.cmp_frecent(other), 39 | } 40 | } 41 | 42 | /// Compare the frequency of two items 43 | fn cmp_frequent(&self, other: &ItemStats) -> Ordering { 44 | self.num_accesses.cmp(&other.num_accesses) 45 | } 46 | 47 | /// Compare the recency of two items 48 | fn cmp_recent(&self, other: &ItemStats) -> Ordering { 49 | self.last_accessed 50 | .partial_cmp(&other.last_accessed) 51 | .unwrap_or(Ordering::Less) 52 | } 53 | 54 | /// Compare the frecency of two items 55 | fn cmp_frecent(&self, other: &ItemStats) -> Ordering { 56 | self.frecency 57 | .partial_cmp(&other.frecency) 58 | .unwrap_or(Ordering::Less) 59 | } 60 | 61 | /// Change the half life of the item, maintaining the same frecency 62 | pub fn set_half_life(&mut self, half_life: f64) { 63 | let old_frecency = self.get_frecency(current_time_secs()); 64 | self.half_life = half_life; 65 | self.set_frecency(old_frecency); 66 | } 67 | 68 | /// Calculate the frecency of the item 69 | pub fn get_frecency(&self, current_time_secs: f64) -> f64 { 70 | self.frecency / 2.0f64.powf((current_time_secs - self.reference_time) / self.half_life) 71 | } 72 | 73 | pub fn set_frecency(&mut self, new: f64) { 74 | self.frecency = 75 | new * 2.0f64.powf((current_time_secs() - self.reference_time) / self.half_life); 76 | } 77 | 78 | /// update the frecency of the item by the given weight 79 | pub fn update_frecency(&mut self, weight: f64) { 80 | let original_frecency = self.get_frecency(current_time_secs()); 81 | self.set_frecency(original_frecency + weight); 82 | } 83 | 84 | /// Update the number of accesses of the item by the given weight 85 | pub fn update_num_accesses(&mut self, weight: i32) { 86 | self.num_accesses += weight; 87 | } 88 | 89 | /// Update the time the item was last accessed 90 | pub fn update_last_access(&mut self, time: f64) { 91 | self.last_accessed = time - self.reference_time; 92 | } 93 | 94 | /// Reset the reference time and recalculate the last_accessed time 95 | pub fn reset_ref_time(&mut self, new_time: f64) { 96 | let original_frecency = self.get_frecency(current_time_secs()); 97 | let delta = self.reference_time - new_time; 98 | self.reference_time = new_time; 99 | self.last_accessed += delta; 100 | self.set_frecency(original_frecency); 101 | } 102 | 103 | /// Timestamp (in nanoseconds since epoch) of the last access 104 | pub fn last_access(&self) -> f64 { 105 | self.reference_time + self.last_accessed 106 | } 107 | } 108 | 109 | /// The number of seconds elapsed since `ref_time` 110 | pub fn secs_elapsed(ref_time: f64) -> f64 { 111 | current_time_secs() - ref_time 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use crate::store::write_stat; 117 | 118 | use super::*; 119 | 120 | fn create_item() -> ItemStats { 121 | let test_item = "/test/item".to_string(); 122 | 123 | ItemStats { 124 | half_life: 100.0, 125 | reference_time: current_time_secs(), 126 | item: test_item.clone(), 127 | frecency: 0.0, 128 | last_accessed: 0.0, 129 | num_accesses: 0, 130 | } 131 | } 132 | 133 | #[test] 134 | fn new_item_stats() { 135 | let test_item = "/test/item"; 136 | let ref_time = current_time_secs(); 137 | 138 | let new_item_stats = ItemStats::new(test_item.to_string(), ref_time, 10.0); 139 | 140 | assert_eq!(new_item_stats.frecency, 0.0); 141 | assert_eq!(new_item_stats.num_accesses, 0); 142 | assert_eq!(new_item_stats.frecency, 0.0); 143 | } 144 | 145 | #[test] 146 | fn compare_with_func() { 147 | let low_item_stats = create_item(); 148 | let mut high_item_stats = create_item(); 149 | 150 | high_item_stats.frecency = 1.0; 151 | high_item_stats.last_accessed = 1.0; 152 | high_item_stats.num_accesses = 1; 153 | 154 | assert_eq!(Ordering::Less, low_item_stats.cmp_frecent(&high_item_stats)); 155 | assert_eq!(Ordering::Less, low_item_stats.cmp_recent(&high_item_stats)); 156 | assert_eq!( 157 | Ordering::Less, 158 | low_item_stats.cmp_frequent(&high_item_stats) 159 | ); 160 | } 161 | 162 | #[test] 163 | fn compare_with_enum() { 164 | let low_item_stats = create_item(); 165 | let mut high_item_stats = create_item(); 166 | 167 | high_item_stats.frecency = 1.0; 168 | high_item_stats.last_accessed = 1.0; 169 | high_item_stats.num_accesses = 1; 170 | 171 | assert_eq!( 172 | Ordering::Less, 173 | low_item_stats.cmp_score(&high_item_stats, SortMethod::Frecent) 174 | ); 175 | assert_eq!( 176 | Ordering::Less, 177 | low_item_stats.cmp_score(&high_item_stats, SortMethod::Recent) 178 | ); 179 | assert_eq!( 180 | Ordering::Less, 181 | low_item_stats.cmp_score(&high_item_stats, SortMethod::Frequent) 182 | ); 183 | } 184 | 185 | #[test] 186 | fn update_score() { 187 | let mut stats = create_item(); 188 | 189 | stats.update_frecency(1.0); 190 | 191 | assert!((stats.frecency - 1.0).abs() < 0.01); 192 | assert_eq!(stats.num_accesses, 0); 193 | } 194 | 195 | #[test] 196 | fn update_num_accesses() { 197 | let mut stats = create_item(); 198 | 199 | stats.update_num_accesses(1); 200 | 201 | assert_eq!(stats.num_accesses, 1); 202 | assert!((stats.frecency.abs() - 0.0) < 0.01); 203 | } 204 | 205 | #[test] 206 | fn to_string_no_stats() { 207 | let stats = create_item(); 208 | 209 | let t = current_time_secs(); 210 | for method in [ 211 | SortMethod::Frecent, 212 | SortMethod::Frequent, 213 | SortMethod::Recent, 214 | ] { 215 | let mut b = Vec::new(); 216 | write_stat(&mut b, &stats, method, false, t, None).unwrap(); 217 | assert_eq!(b, String::from("/test/item\n").into_bytes()); 218 | } 219 | } 220 | 221 | #[test] 222 | fn to_string_stats() { 223 | let stats = create_item(); 224 | 225 | let t = current_time_secs(); 226 | for (method, expected) in [ 227 | (SortMethod::Frecent, String::from("0.000\t/test/item\n")), 228 | (SortMethod::Recent, String::from("0.000\t/test/item\n")), 229 | (SortMethod::Frequent, String::from("0\t/test/item\n")), 230 | ] { 231 | let mut b = Vec::new(); 232 | write_stat(&mut b, &stats, method, true, t, None).unwrap(); 233 | assert_eq!(String::from_utf8(b).unwrap(), expected); 234 | } 235 | } 236 | 237 | #[test] 238 | fn to_string_custom_precision() { 239 | let t = current_time_secs(); 240 | let mut stats = create_item(); 241 | stats.reference_time = t - 100.654321; 242 | stats.last_accessed = 50.123456; 243 | stats.num_accesses = 15; 244 | stats.frecency = 320.123456; 245 | 246 | for (method, expected) in [ 247 | (SortMethod::Frecent, String::from("159.33743\t/test/item\n")), 248 | (SortMethod::Recent, String::from("0.01404\t/test/item\n")), 249 | (SortMethod::Frequent, String::from("15.00000\t/test/item\n")), 250 | ] { 251 | let mut b = Vec::new(); 252 | write_stat(&mut b, &stats, method, true, t, Some(5)).unwrap(); 253 | assert_eq!(String::from_utf8(b).unwrap(), expected); 254 | } 255 | } 256 | 257 | #[test] 258 | fn get_frecency_one_half_life() { 259 | let mut stats = create_item(); 260 | 261 | let t = current_time_secs(); 262 | stats.reset_ref_time(t - 1.0 * stats.half_life); 263 | stats.frecency = 1.0; 264 | 265 | assert!((stats.get_frecency(t) - 0.5).abs() < 0.01); 266 | } 267 | 268 | #[test] 269 | fn get_frecency_two_half_lives() { 270 | let mut stats = create_item(); 271 | 272 | let t = current_time_secs(); 273 | stats.reset_ref_time(current_time_secs() - 2.0 * stats.half_life); 274 | stats.frecency = 1.0; 275 | 276 | assert!((stats.get_frecency(t) - 0.25).abs() < 0.01); 277 | } 278 | 279 | #[test] 280 | fn reset_time() { 281 | let mut low_item_stats = create_item(); 282 | let current_time = current_time_secs(); 283 | low_item_stats.reference_time = current_time - 5.0; 284 | low_item_stats.last_accessed = 10.0; 285 | 286 | low_item_stats.reset_ref_time(current_time); 287 | 288 | assert!((low_item_stats.reference_time - current_time).abs() < 0.1); 289 | assert!((low_item_stats.last_accessed - 5.0).abs() < 0.1); 290 | } 291 | 292 | #[test] 293 | fn set_half_life() { 294 | let mut low_item_stats = create_item(); 295 | let current_time = current_time_secs(); 296 | low_item_stats.reference_time = current_time - 2.0; 297 | low_item_stats.last_accessed = 1.0; 298 | let original_frecency = low_item_stats.get_frecency(current_time); 299 | 300 | low_item_stats.set_half_life(2.0); 301 | 302 | assert_eq!(low_item_stats.half_life, 2.0); 303 | assert!((low_item_stats.get_frecency(current_time) - original_frecency).abs() < 0.01); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/stats/serialize.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | pub struct ItemStatsSerializer { 5 | pub item: String, 6 | pub frecency: f64, 7 | pub last_accessed: f64, 8 | pub num_accesses: i32, 9 | } 10 | 11 | impl From for ItemStatsSerializer { 12 | fn from(stats: ItemStats) -> Self { 13 | ItemStatsSerializer { 14 | item: stats.item, 15 | frecency: stats.frecency, 16 | last_accessed: stats.last_accessed, 17 | num_accesses: stats.num_accesses, 18 | } 19 | } 20 | } 21 | 22 | impl ItemStatsSerializer { 23 | pub fn into_item_stats(self, ref_time: f64, half_life: f64) -> ItemStats { 24 | ItemStats { 25 | half_life, 26 | reference_time: ref_time, 27 | item: self.item, 28 | frecency: self.frecency, 29 | last_accessed: self.last_accessed, 30 | num_accesses: self.num_accesses, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/store/mod.rs: -------------------------------------------------------------------------------- 1 | mod serialize; 2 | 3 | use super::current_time_secs; 4 | use super::stats::ItemStats; 5 | use crate::args::SortMethod; 6 | use anyhow::Result; 7 | use std::default::Default; 8 | use std::fs::{self, File}; 9 | use std::io::{self, BufReader, BufWriter, Write}; 10 | use std::path::PathBuf; 11 | 12 | /// Parses the file at `path` into a `UsageStore` object 13 | pub fn read_store(path: &PathBuf) -> Result { 14 | if path.is_file() { 15 | let file = File::open(path)?; 16 | let reader = BufReader::new(file); 17 | let store: serialize::FrecencyStoreSerializer = serde_json::from_reader(reader)?; 18 | Ok(FrecencyStore::from(store)) 19 | } else { 20 | Ok(FrecencyStore::default()) 21 | } 22 | } 23 | 24 | /// Serializes and writes a `UsageStore` to a file 25 | pub fn write_store(store: FrecencyStore, path: &PathBuf) -> io::Result<()> { 26 | let store_dir = path.parent().expect("file must have parent"); 27 | fs::create_dir_all(store_dir)?; 28 | let file = File::create(path)?; 29 | let writer = BufWriter::new(file); 30 | serde_json::to_writer_pretty(writer, &serialize::FrecencyStoreSerializer::from(store))?; 31 | 32 | Ok(()) 33 | } 34 | 35 | /// A collection of statistics about the stored items 36 | pub struct FrecencyStore { 37 | reference_time: f64, 38 | half_life: f64, 39 | pub items: Vec, 40 | } 41 | 42 | impl Default for FrecencyStore { 43 | fn default() -> FrecencyStore { 44 | FrecencyStore { 45 | reference_time: current_time_secs(), 46 | half_life: 60.0 * 60.0 * 24.0 * 3.0, // three day half life 47 | items: Vec::new(), 48 | } 49 | } 50 | } 51 | 52 | impl FrecencyStore { 53 | /// Remove all but the top N (sorted by `sort_method`) from the `UsageStore` 54 | pub fn truncate(&mut self, keep_num: usize, sort_method: SortMethod) { 55 | let mut sorted_vec = self.sorted(sort_method); 56 | sorted_vec.truncate(keep_num); 57 | self.items = sorted_vec; 58 | } 59 | 60 | /// Change the half life and reweight such that frecency does not change 61 | pub fn set_half_life(&mut self, half_life: f64) { 62 | self.reset_time(); 63 | self.half_life = half_life; 64 | 65 | for item in self.items.iter_mut() { 66 | item.set_half_life(half_life); 67 | } 68 | } 69 | 70 | /// Return the number of half lives passed since the reference time 71 | pub fn half_lives_passed(&self) -> f64 { 72 | (current_time_secs() - self.reference_time) / self.half_life 73 | } 74 | 75 | /// Reset the reference time to now, and reweight all the statistics to reflect that 76 | pub fn reset_time(&mut self) { 77 | let current_time = current_time_secs(); 78 | 79 | self.reference_time = current_time; 80 | 81 | for item in self.items.iter_mut() { 82 | item.reset_ref_time(current_time); 83 | } 84 | } 85 | 86 | /// Log a visit to a item 87 | pub fn add(&mut self, item: &str) { 88 | let item_stats = self.get(item); 89 | 90 | item_stats.update_frecency(1.0); 91 | item_stats.update_num_accesses(1); 92 | item_stats.update_last_access(current_time_secs()); 93 | } 94 | 95 | /// Adjust the score of a item by a given weight 96 | pub fn adjust(&mut self, item: &str, weight: f64) { 97 | let item_stats = self.get(item); 98 | 99 | item_stats.update_frecency(weight); 100 | item_stats.update_num_accesses(weight as i32); 101 | } 102 | 103 | /// Delete an item from the store 104 | pub fn delete(&mut self, item: &str) { 105 | if let Some(idx) = self.items.iter().position(|i| i.item == item) { 106 | self.items.remove(idx); 107 | } 108 | } 109 | 110 | /// Return a sorted vector of all the items in the store, sorted by `sort_method` 111 | pub fn sorted(&self, sort_method: SortMethod) -> Vec { 112 | let mut new_vec = self.items.clone(); 113 | new_vec.sort_by(|item1, item2| item1.cmp_score(item2, sort_method).reverse()); 114 | 115 | new_vec 116 | } 117 | 118 | /// Retrieve a mutable reference to a item in the store. 119 | /// If the item does not exist, create it and return a reference to the created item 120 | fn get(&mut self, item: &str) -> &mut ItemStats { 121 | match self 122 | .items 123 | .binary_search_by_key(&item, |item_stats| &item_stats.item) 124 | { 125 | Ok(idx) => &mut self.items[idx], 126 | Err(idx) => { 127 | self.items.insert( 128 | idx, 129 | ItemStats::new(item.to_string(), self.reference_time, self.half_life), 130 | ); 131 | &mut self.items[idx] 132 | } 133 | } 134 | } 135 | } 136 | 137 | /// Print out all the items, sorted by `method`, with an optional maximum of `limit` 138 | pub fn write_stats( 139 | w: &mut W, 140 | items: &[ItemStats], 141 | method: SortMethod, 142 | show_stats: bool, 143 | current_time: f64, 144 | precision_override: Option, 145 | ) -> Result<()> { 146 | for item in items { 147 | write_stat( 148 | w, 149 | item, 150 | method, 151 | show_stats, 152 | current_time, 153 | precision_override, 154 | )?; 155 | } 156 | Ok(()) 157 | } 158 | 159 | pub fn write_stat( 160 | w: &mut W, 161 | item: &ItemStats, 162 | method: SortMethod, 163 | show_stats: bool, 164 | current_time: f64, 165 | precision_override: Option, 166 | ) -> Result<()> { 167 | if show_stats { 168 | let (score, default_precision) = match method { 169 | SortMethod::Recent => ((current_time - item.last_access()) / 60.0 / 60.0, 3), 170 | SortMethod::Frequent => (item.num_accesses as f64, 0), 171 | SortMethod::Frecent => (item.get_frecency(current_time), 3), 172 | }; 173 | let precision = precision_override.unwrap_or(default_precision); 174 | w.write_fmt(format_args!( 175 | "{: <.prec$}\t{}\n", 176 | score, 177 | item.item, 178 | prec = precision 179 | ))?; 180 | } else { 181 | w.write_fmt(format_args!("{}\n", &item.item))?; 182 | } 183 | Ok(()) 184 | } 185 | 186 | #[cfg(test)] 187 | mod tests { 188 | use super::*; 189 | 190 | fn create_usage() -> FrecencyStore { 191 | FrecencyStore { 192 | reference_time: current_time_secs(), 193 | half_life: 1.0, 194 | items: Vec::new(), 195 | } 196 | } 197 | 198 | #[test] 199 | fn add_new() { 200 | let mut usage = create_usage(); 201 | 202 | usage.add("test"); 203 | 204 | assert_eq!(1, usage.items.len()); 205 | } 206 | 207 | #[test] 208 | fn add_existing() { 209 | let mut usage = create_usage(); 210 | 211 | usage.add("test"); 212 | usage.add("test"); 213 | 214 | assert_eq!(1, usage.items.len()); 215 | } 216 | 217 | #[test] 218 | fn delete_existing() { 219 | let mut usage = create_usage(); 220 | usage.add("test"); 221 | assert_eq!(usage.items.len(), 1); 222 | usage.delete("test"); 223 | assert_eq!(usage.items.len(), 0); 224 | } 225 | 226 | #[test] 227 | fn delete_nonexisting() { 228 | let mut usage = create_usage(); 229 | usage.delete("test"); 230 | assert_eq!(usage.items.len(), 0); 231 | } 232 | 233 | #[test] 234 | fn adjust_existing() { 235 | let mut usage = create_usage(); 236 | 237 | usage.add("test"); 238 | usage.adjust("test", 3.0); 239 | 240 | assert_eq!(usage.items.len(), 1); 241 | } 242 | 243 | #[test] 244 | fn adjust_new() { 245 | let mut usage = create_usage(); 246 | 247 | usage.adjust("test", 3.0); 248 | 249 | assert_eq!(usage.items.len(), 1); 250 | } 251 | 252 | #[test] 253 | fn truncate_greater() { 254 | let mut usage = create_usage(); 255 | usage.add("dir1"); 256 | usage.add("dir2"); 257 | 258 | usage.truncate(1, SortMethod::Recent); 259 | 260 | assert_eq!(usage.items.len(), 1); 261 | } 262 | 263 | #[test] 264 | fn truncate_less() { 265 | let mut usage = create_usage(); 266 | usage.add("dir1"); 267 | usage.add("dir2"); 268 | 269 | usage.truncate(3, SortMethod::Recent); 270 | 271 | assert_eq!(usage.items.len(), 2); 272 | } 273 | 274 | #[test] 275 | fn sorted_frecent() { 276 | let mut usage = create_usage(); 277 | usage.add("dir1"); 278 | usage.add("dir2"); 279 | usage.get("dir2").update_frecency(1000.0); 280 | 281 | let sorted = usage.sorted(SortMethod::Frecent); 282 | 283 | assert_eq!(sorted.len(), 2); 284 | assert_eq!(sorted[0].item, "dir2".to_string()); 285 | } 286 | 287 | #[test] 288 | fn sorted_frecent2() { 289 | let mut usage = create_usage(); 290 | usage.add("dir1"); 291 | usage.add("dir2"); 292 | usage.get("dir1").update_frecency(1000.0); 293 | 294 | let sorted = usage.sorted(SortMethod::Frecent); 295 | 296 | assert_eq!(sorted.len(), 2); 297 | assert_eq!(sorted[0].item, "dir1".to_string()); 298 | } 299 | 300 | #[test] 301 | fn sorted_recent() { 302 | let mut usage = create_usage(); 303 | usage.add("dir1"); 304 | usage.add("dir2"); 305 | usage 306 | .get("dir2") 307 | .update_last_access(current_time_secs() + 100.0); 308 | 309 | let sorted = usage.sorted(SortMethod::Recent); 310 | 311 | assert_eq!(sorted.len(), 2); 312 | assert_eq!(sorted[0].item, "dir2".to_string()); 313 | } 314 | 315 | #[test] 316 | fn sorted_frequent() { 317 | let mut usage = create_usage(); 318 | usage.add("dir1"); 319 | usage.add("dir2"); 320 | usage.get("dir2").update_num_accesses(100); 321 | 322 | let sorted = usage.sorted(SortMethod::Frequent); 323 | 324 | assert_eq!(sorted.len(), 2); 325 | assert_eq!(sorted[0].item, "dir2".to_string()); 326 | } 327 | 328 | #[test] 329 | fn get_exists() { 330 | let mut usage = create_usage(); 331 | usage.add("dir1"); 332 | 333 | let _stats = usage.get("dir1"); 334 | 335 | assert_eq!(usage.items.len(), 1); 336 | } 337 | 338 | #[test] 339 | fn get_not_exists() { 340 | let mut usage = create_usage(); 341 | usage.add("dir1"); 342 | 343 | usage.get("dir2"); 344 | 345 | assert_eq!(usage.items.len(), 2); 346 | } 347 | 348 | #[test] 349 | fn reset_time() { 350 | let mut usage = create_usage(); 351 | let current_time = current_time_secs(); 352 | usage.reference_time = current_time - 10.0; 353 | usage.add("test"); 354 | let original_frecency = usage.get("test").get_frecency(current_time); 355 | 356 | usage.reset_time(); 357 | 358 | assert!((usage.reference_time - current_time).abs() < 0.01); 359 | assert!((usage.get("test").get_frecency(current_time) - original_frecency).abs() < 0.01); 360 | } 361 | 362 | #[test] 363 | fn set_halflife() { 364 | let mut usage = create_usage(); 365 | let current_time = current_time_secs(); 366 | usage.reference_time = current_time - 10.0; 367 | usage.add("dir1"); 368 | let original_frecency = usage.get("dir1").get_frecency(current_time); 369 | usage.set_half_life(10.0); 370 | 371 | let new_frecency = usage.get("dir1").get_frecency(current_time); 372 | 373 | assert!((usage.half_life - 10.0).abs() < 0.01); 374 | assert!((new_frecency - original_frecency).abs() < 0.01); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/store/serialize.rs: -------------------------------------------------------------------------------- 1 | use super::super::stats::serialize; 2 | use super::*; 3 | 4 | #[derive(Serialize, Deserialize, Debug)] 5 | pub struct FrecencyStoreSerializer { 6 | reference_time: f64, 7 | half_life: f64, 8 | items: Vec, 9 | } 10 | 11 | impl From for FrecencyStoreSerializer { 12 | fn from(store: FrecencyStore) -> Self { 13 | let items = store 14 | .items 15 | .into_iter() 16 | .map(serialize::ItemStatsSerializer::from) 17 | .collect(); 18 | 19 | FrecencyStoreSerializer { 20 | reference_time: store.reference_time, 21 | half_life: store.half_life, 22 | items, 23 | } 24 | } 25 | } 26 | 27 | impl From for FrecencyStore { 28 | fn from(store: FrecencyStoreSerializer) -> Self { 29 | let ref_time = store.reference_time; 30 | let half_life = store.half_life; 31 | let items = store 32 | .items 33 | .into_iter() 34 | .map(|s| s.into_item_stats(ref_time, half_life)) 35 | .collect(); 36 | 37 | FrecencyStore { 38 | reference_time: store.reference_time, 39 | half_life: store.half_life, 40 | items, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use fre::current_time_secs; 2 | use predicates::*; 3 | use std::collections::HashMap; 4 | use std::io::Write; 5 | use std::str; 6 | use tempfile; 7 | 8 | pub fn get_tempfile_path() -> tempfile::TempPath { 9 | let mut file = tempfile::NamedTempFile::new().unwrap(); 10 | 11 | let current_time = current_time_secs(); 12 | 13 | file.write( 14 | format!( 15 | r#"{{ 16 | "reference_time": {}, 17 | "half_life": 259200.0, 18 | "items": [ 19 | {{ 20 | "item": "/home", 21 | "frecency": 3.0, 22 | "last_accessed": -100.0, 23 | "num_accesses": 2 24 | }}, 25 | {{ 26 | "item": "/home/nonexistant_dir", 27 | "frecency": 2.0, 28 | "last_accessed": 1.0, 29 | "num_accesses": 1 30 | }}, 31 | {{ 32 | "item": "/", 33 | "frecency": 1.0, 34 | "last_accessed": 0.0, 35 | "num_accesses": 3 36 | }} 37 | ] 38 | }}"#, 39 | current_time 40 | ) 41 | .as_bytes(), 42 | ) 43 | .unwrap(); 44 | 45 | return file.into_temp_path(); 46 | } 47 | 48 | pub fn parse_scored_output(output: &str) -> Option> { 49 | use std::f64; 50 | 51 | let mut out_map = HashMap::new(); 52 | for line in output.lines() { 53 | let mut elems = line.split_whitespace(); 54 | let score: f64 = elems 55 | .next() 56 | .expect("no score on this line") 57 | .parse::() 58 | .unwrap(); 59 | let item = elems.next().expect("no item on this line"); 60 | out_map.insert(item.to_string(), score); 61 | } 62 | 63 | return Some(out_map); 64 | } 65 | 66 | pub fn item_score_approx_equal(item: String, expected: f64) -> impl Predicate<[u8]> { 67 | predicates::function::function(move |x: &[u8]| { 68 | let map = parse_scored_output(str::from_utf8(x).expect("failed to parse utf8")); 69 | let out_score = map 70 | .expect("failed to parse scored output") 71 | .get(&item.clone()) 72 | .expect("item doesn't exist in output") 73 | .clone(); 74 | out_score >= expected * 0.95 && out_score <= expected * 1.05 75 | }) 76 | } 77 | 78 | pub fn n_results(n: usize) -> impl Predicate<[u8]> { 79 | predicates::function::function(move |x: &[u8]| str::from_utf8(x).unwrap().lines().count() == n) 80 | } 81 | -------------------------------------------------------------------------------- /tests/integration/errors.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use predicates::prelude::*; 3 | use std::process::Command; 4 | use tempfile; 5 | 6 | #[test] 7 | fn invalid_store() { 8 | let empty = predicates::str::is_empty().from_utf8(); 9 | let error = predicates::str::contains("failed to read store file").from_utf8(); 10 | let file = tempfile::NamedTempFile::new().unwrap(); 11 | 12 | Command::main_binary() 13 | .unwrap() 14 | .arg("--store") 15 | .arg(file.path().as_os_str()) 16 | .arg("--sorted") 17 | .assert() 18 | .code(1) 19 | .stdout(empty) 20 | .stderr(error); 21 | } 22 | 23 | #[test] 24 | fn non_writable() { 25 | let empty = predicates::str::is_empty().from_utf8(); 26 | let error = predicates::str::is_empty().from_utf8().not(); 27 | 28 | Command::main_binary() 29 | .unwrap() 30 | .arg("--store") 31 | .arg("/testdir") 32 | .arg("--sorted") 33 | .assert() 34 | .code(1) 35 | .stdout(empty) 36 | .stderr(error); 37 | } 38 | -------------------------------------------------------------------------------- /tests/integration/mod.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | mod sort; 3 | mod weight; 4 | 5 | use super::common; 6 | -------------------------------------------------------------------------------- /tests/integration/sort.rs: -------------------------------------------------------------------------------- 1 | use super::common; 2 | use assert_cmd::prelude::*; 3 | use predicates::prelude::*; 4 | use std::process::Command; 5 | use std::str; 6 | 7 | #[test] 8 | fn sorted_stats() { 9 | let store_file = common::get_tempfile_path(); 10 | 11 | let expected_sorted = 12 | predicate::str::similar("3\t/\n2\t/home\n1\t/home/nonexistant_dir\n").from_utf8(); 13 | 14 | Command::main_binary() 15 | .unwrap() 16 | .arg("--store") 17 | .arg(&store_file.as_os_str()) 18 | .arg("--stat") 19 | .arg("--sort_method") 20 | .arg("frequent") 21 | .assert() 22 | .stdout(expected_sorted); 23 | } 24 | 25 | #[test] 26 | fn sorted_frecent() { 27 | let store_file = common::get_tempfile_path(); 28 | 29 | let expected_sorted = predicate::str::similar("/home\n/home/nonexistant_dir\n/\n").from_utf8(); 30 | 31 | Command::main_binary() 32 | .unwrap() 33 | .arg("--store") 34 | .arg(&store_file.as_os_str()) 35 | .arg("--sorted") 36 | .assert() 37 | .stdout(expected_sorted); 38 | } 39 | 40 | #[test] 41 | fn sorted_recent() { 42 | let store_file = common::get_tempfile_path(); 43 | 44 | let expected_sorted = predicate::str::similar("/home/nonexistant_dir\n/\n/home\n").from_utf8(); 45 | 46 | Command::main_binary() 47 | .unwrap() 48 | .arg("--store") 49 | .arg(&store_file.as_os_str()) 50 | .arg("--sorted") 51 | .arg("--sort_method") 52 | .arg("recent") 53 | .assert() 54 | .stdout(expected_sorted); 55 | } 56 | 57 | #[test] 58 | fn sorted_frequent() { 59 | let store_file = common::get_tempfile_path(); 60 | 61 | let expected_sorted = predicate::str::similar("/\n/home\n/home/nonexistant_dir\n").from_utf8(); 62 | 63 | Command::main_binary() 64 | .unwrap() 65 | .arg("--store") 66 | .arg(&store_file.as_os_str()) 67 | .arg("--sorted") 68 | .arg("--sort_method") 69 | .arg("frequent") 70 | .assert() 71 | .stdout(expected_sorted); 72 | } 73 | 74 | #[test] 75 | fn sorted_invalid() { 76 | let store_file = common::get_tempfile_path(); 77 | 78 | let expected_error = predicate::str::contains("invalid value 'badsort'").from_utf8(); 79 | 80 | Command::main_binary() 81 | .unwrap() 82 | .arg("--store") 83 | .arg(&store_file.as_os_str()) 84 | .arg("--sorted") 85 | .arg("--sort_method") 86 | .arg("badsort") 87 | .assert() 88 | .stderr(expected_error); 89 | } 90 | 91 | #[test] 92 | fn truncate() { 93 | let store_file = common::get_tempfile_path(); 94 | 95 | Command::main_binary() 96 | .unwrap() 97 | .arg("--store") 98 | .arg(&store_file.as_os_str()) 99 | .arg("--truncate") 100 | .arg("2") 101 | .assert() 102 | .success(); 103 | 104 | let two_lines = predicate::function(|x: &[u8]| str::from_utf8(x).unwrap().lines().count() == 2); 105 | 106 | Command::main_binary() 107 | .unwrap() 108 | .arg("--store") 109 | .arg(&store_file.as_os_str()) 110 | .arg("--stat") 111 | .assert() 112 | .stdout(two_lines); 113 | } 114 | 115 | #[test] 116 | fn limit() { 117 | let store_file = common::get_tempfile_path(); 118 | 119 | let two_lines = common::n_results(2); 120 | 121 | Command::main_binary() 122 | .unwrap() 123 | .arg("--store") 124 | .arg(&store_file.as_os_str()) 125 | .arg("--sorted") 126 | .arg("--limit") 127 | .arg("2") 128 | .assert() 129 | .success() 130 | .stdout(two_lines); 131 | } 132 | 133 | #[test] 134 | fn limit_too_many() { 135 | let store_file = common::get_tempfile_path(); 136 | 137 | let three_lines = common::n_results(3); 138 | 139 | Command::main_binary() 140 | .unwrap() 141 | .arg("--store") 142 | .arg(&store_file.as_os_str()) 143 | .arg("--sorted") 144 | .arg("--limit") 145 | .arg("4") 146 | .assert() 147 | .success() 148 | .stdout(three_lines); 149 | } 150 | 151 | #[test] 152 | fn change_half_life_maintain_frecency() { 153 | let store_file = common::get_tempfile_path(); 154 | 155 | Command::main_binary() 156 | .unwrap() 157 | .arg("--store") 158 | .arg(&store_file.as_os_str()) 159 | .arg("--halflife") 160 | .arg("1000") 161 | .assert() 162 | .success(); 163 | 164 | let score_same = common::item_score_approx_equal("/".to_string(), 1.0); 165 | 166 | Command::main_binary() 167 | .unwrap() 168 | .arg("--store") 169 | .arg(&store_file.as_os_str()) 170 | .arg("--stat") 171 | .assert() 172 | .stdout(score_same); 173 | } 174 | 175 | #[test] 176 | fn change_half_life_new_decay() { 177 | let store_file = common::get_tempfile_path(); 178 | 179 | Command::main_binary() 180 | .unwrap() 181 | .arg("--store") 182 | .arg(&store_file.as_os_str()) 183 | .arg("--halflife") 184 | .arg("100.0") 185 | .assert() 186 | .success(); 187 | 188 | let score_half = common::item_score_approx_equal("/home".to_string(), 3.0); 189 | 190 | Command::main_binary() 191 | .unwrap() 192 | .arg("--store") 193 | .arg(&store_file.as_os_str()) 194 | .arg("--stat") 195 | .assert() 196 | .stdout(score_half); 197 | } 198 | -------------------------------------------------------------------------------- /tests/integration/weight.rs: -------------------------------------------------------------------------------- 1 | use super::common; 2 | use assert_cmd::prelude::*; 3 | use predicates::prelude::*; 4 | use std::process::Command; 5 | 6 | #[test] 7 | fn add_existing_exists() { 8 | let store_file = common::get_tempfile_path(); 9 | let dir = "/home".to_string(); 10 | 11 | Command::main_binary() 12 | .unwrap() 13 | .arg("--store") 14 | .arg(&store_file.as_os_str()) 15 | .arg("--add") 16 | .arg(&dir) 17 | .assert(); 18 | 19 | let exists = predicates::str::contains(dir).from_utf8(); 20 | 21 | Command::main_binary() 22 | .unwrap() 23 | .arg("--store") 24 | .arg(&store_file.as_os_str()) 25 | .arg("--sorted") 26 | .assert() 27 | .stdout(exists); 28 | } 29 | 30 | #[test] 31 | fn add_existing_increases() { 32 | let store_file = common::get_tempfile_path(); 33 | let dir = "/home".to_string(); 34 | 35 | Command::main_binary() 36 | .unwrap() 37 | .arg("--store") 38 | .arg(&store_file.as_os_str()) 39 | .arg("--add") 40 | .arg(&dir) 41 | .assert(); 42 | 43 | let increased = common::item_score_approx_equal(dir, 3.0); 44 | 45 | Command::main_binary() 46 | .unwrap() 47 | .arg("--store") 48 | .arg(&store_file.as_os_str()) 49 | .arg("--stat") 50 | .arg("--sort_method") 51 | .arg("frequent") 52 | .assert() 53 | .stdout(increased); 54 | } 55 | 56 | #[test] 57 | fn add_create() { 58 | let store_file = common::get_tempfile_path(); 59 | let new_dir = "/home/super_new_dir".to_string(); 60 | 61 | Command::main_binary() 62 | .unwrap() 63 | .arg("--store") 64 | .arg(&store_file.as_os_str()) 65 | .arg("--add") 66 | .arg(&new_dir) 67 | .assert() 68 | .success(); 69 | 70 | let exists = predicates::str::contains(new_dir).from_utf8(); 71 | 72 | Command::main_binary() 73 | .unwrap() 74 | .arg("--store") 75 | .arg(&store_file.as_os_str()) 76 | .arg("--sorted") 77 | .assert() 78 | .stdout(exists); 79 | } 80 | 81 | #[test] 82 | fn increase_accesses() { 83 | let store_file = common::get_tempfile_path(); 84 | let absolute_dir = "/home".to_string(); 85 | 86 | Command::main_binary() 87 | .unwrap() 88 | .arg("--store") 89 | .arg(&store_file.as_os_str()) 90 | .arg("--increase") 91 | .arg("2.0") 92 | .arg(&absolute_dir) 93 | .assert() 94 | .success(); 95 | 96 | let accesses_increased_two = common::item_score_approx_equal(absolute_dir.clone(), 4.0); 97 | 98 | Command::main_binary() 99 | .unwrap() 100 | .arg("--store") 101 | .arg(&store_file.as_os_str()) 102 | .arg("--stat") 103 | .arg("--sort_method") 104 | .arg("frequent") 105 | .assert() 106 | .stdout(accesses_increased_two); 107 | } 108 | 109 | #[test] 110 | fn decrease_accesses() { 111 | let store_file = common::get_tempfile_path(); 112 | let absolute_dir = "/home".to_string(); 113 | 114 | Command::main_binary() 115 | .unwrap() 116 | .current_dir(std::env::temp_dir().as_os_str()) 117 | .arg("--store") 118 | .arg(&store_file.as_os_str()) 119 | .arg("--decrease") 120 | .arg("1.0") 121 | .arg(&absolute_dir) 122 | .assert() 123 | .success(); 124 | 125 | let accesses_decreased_one = common::item_score_approx_equal(absolute_dir.clone(), 1.0); 126 | 127 | Command::main_binary() 128 | .unwrap() 129 | .current_dir(std::env::temp_dir().as_os_str()) 130 | .arg("--store") 131 | .arg(&store_file.as_os_str()) 132 | .arg("--stat") 133 | .arg("--sort_method") 134 | .arg("frequent") 135 | .assert() 136 | .stdout(accesses_decreased_one); 137 | } 138 | 139 | #[test] 140 | fn increase_score() { 141 | let store_file = common::get_tempfile_path(); 142 | let absolute_dir = "/home".to_string(); 143 | 144 | Command::main_binary() 145 | .unwrap() 146 | .current_dir(std::env::temp_dir().as_os_str()) 147 | .arg("--store") 148 | .arg(&store_file.as_os_str()) 149 | .arg("--increase") 150 | .arg("2.0") 151 | .arg(&absolute_dir) 152 | .assert() 153 | .success(); 154 | 155 | let frecency_increased_two = common::item_score_approx_equal(absolute_dir.clone(), 5.0); 156 | 157 | Command::main_binary() 158 | .unwrap() 159 | .current_dir(std::env::temp_dir().as_os_str()) 160 | .arg("--store") 161 | .arg(&store_file.as_os_str()) 162 | .arg("--stat") 163 | .arg("--sort_method") 164 | .arg("frecent") 165 | .assert() 166 | .stdout(frecency_increased_two); 167 | } 168 | 169 | #[test] 170 | fn decrease_score() { 171 | let store_file = common::get_tempfile_path(); 172 | let absolute_dir = "/home".to_string(); 173 | 174 | Command::main_binary() 175 | .unwrap() 176 | .current_dir(std::env::temp_dir().as_os_str()) 177 | .arg("--store") 178 | .arg(&store_file.as_os_str()) 179 | .arg("--decrease") 180 | .arg("1.0") 181 | .arg(&absolute_dir) 182 | .assert() 183 | .success(); 184 | 185 | let frecency_decreased_one = common::item_score_approx_equal(absolute_dir.clone(), 2.0); 186 | 187 | Command::main_binary() 188 | .unwrap() 189 | .current_dir(std::env::temp_dir().as_os_str()) 190 | .arg("--store") 191 | .arg(&store_file.as_os_str()) 192 | .arg("--stat") 193 | .arg("--sort_method") 194 | .arg("frecent") 195 | .assert() 196 | .stdout(frecency_decreased_one); 197 | } 198 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod integration; 3 | --------------------------------------------------------------------------------