Showing preview only (276K chars total). Download the full file or copy to clipboard to get everything.
Repository: cantino/mcfly
Branch: master
Commit: b6c4d46a826c
Files: 50
Total size: 262.0 KB
Directory structure:
gitextract_5xr7nxho/
├── .github/
│ └── workflows/
│ ├── clippy.yml
│ ├── mean_bean_ci.yml
│ ├── mean_bean_deploy.yml
│ └── rustfmt.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.txt
├── Cargo.toml
├── LICENSE
├── README.md
├── ci/
│ ├── before_deploy.sh
│ ├── build.bash
│ ├── common.bash
│ ├── install.sh
│ ├── script.sh
│ ├── set_rust_version.bash
│ └── test.bash
├── dev.bash
├── dev.fish
├── dev.zsh
├── mcfly.bash
├── mcfly.fish
├── mcfly.ps1
├── mcfly.zsh
├── pkg/
│ └── brew/
│ └── mcfly.rb
└── src/
├── cli.rs
├── command_input.rs
├── dumper.rs
├── fake_typer.rs
├── fixed_length_grapheme_string.rs
├── history/
│ ├── db_extensions.rs
│ ├── history.rs
│ ├── mod.rs
│ └── schema.rs
├── history_cleaner.rs
├── init.rs
├── interface.rs
├── lib.rs
├── main.rs
├── network.rs
├── node.rs
├── path_update_helpers.rs
├── settings.rs
├── shell_history.rs
├── simplified_command.rs
├── stats_generator.rs
├── time.rs
├── trainer.rs
├── training_cache.rs
└── training_sample_generator.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/clippy.yml
================================================
name: clippy
on: [push, pull_request]
jobs:
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- run: cargo clippy -- -D warnings
================================================
FILE: .github/workflows/mean_bean_ci.yml
================================================
name: Mean Bean CI
on: [push, pull_request]
jobs:
install-cross:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 50
- uses: XAMPPRocky/get-github-release@v1
id: cross
with:
owner: rust-embedded
repo: cross
matches: ${{ matrix.platform }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v4
with:
name: cross-${{ matrix.platform }}
path: ${{ steps.cross.outputs.install_path }}
strategy:
matrix:
platform: [linux-musl]
macos:
runs-on: macos-latest
strategy:
fail-fast: true
matrix:
channel: [stable, beta] #, nightly]
target:
- x86_64-apple-darwin
### Disable running tests on M1 target, not currently working
### https://github.com/rust-lang/rust/issues/73908
# - aarch64-apple-darwin
steps:
- name: Setup | Checkout
uses: actions/checkout@v2
- name: Setup | Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
target: ${{ matrix.target }}
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --target ${{ matrix.target }}
use-cross: false
linux:
runs-on: ubuntu-latest
needs: install-cross
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 50
- name: Download Cross
uses: actions/download-artifact@v4
with:
name: cross-linux-musl
path: /tmp/
- run: chmod +x /tmp/cross
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}
- run: ci/build.bash /tmp/cross ${{ matrix.target }}
# These targets have issues with being tested so they are disabled
# by default. You can try disabling to see if they work for
# your project.
- run: ci/test.bash /tmp/cross ${{ matrix.target }}
if: |
!contains(matrix.target, 'android') &&
!contains(matrix.target, 'bsd') &&
!contains(matrix.target, 'solaris') &&
matrix.target != 'armv5te-unknown-linux-musleabi' &&
matrix.target != 'sparc64-unknown-linux-gnu'
strategy:
fail-fast: true
matrix:
channel: [stable, beta] #, nightly]
target:
- arm-unknown-linux-gnueabi
- armv7-unknown-linux-gnueabihf
- i686-unknown-linux-musl
- x86_64-unknown-linux-musl
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- arm-unknown-linux-gnueabihf
================================================
FILE: .github/workflows/mean_bean_deploy.yml
================================================
on:
push:
# # Sequence of patterns matched against refs/tags
tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
name: Mean Bean Deploy
env:
BIN: mcfly
jobs:
# This job downloads and stores `cross` as an artifact, so that it can be
# redownloaded across all of the jobs. Currently this copied pasted between
# `mean_bean_ci.yml` and `mean_bean_deploy.yml`. Make sure to update both places when making
# changes.
install-cross:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 50
- uses: XAMPPRocky/get-github-release@v1
id: cross
with:
owner: rust-embedded
repo: cross
matches: ${{ matrix.platform }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v4
with:
name: cross-${{ matrix.platform }}
path: ${{ steps.cross.outputs.install_path }}
strategy:
matrix:
platform: [linux-musl]
macos:
runs-on: macos-latest
strategy:
matrix:
target:
# macOS
- x86_64-apple-darwin
## Disabling creating M1 builds, will need to wait for github to release M1 VMs
# - aarch64-apple-darwin
steps:
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Setup | Checkout
uses: actions/checkout@v2
# Cache files between builds
- name: Setup | Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ matrix.target }}
- name: Setup | Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
target: ${{ matrix.target }}
- name: Build | Build
uses: actions-rs/cargo@v1
with:
command: build
args: --release --target ${{ matrix.target }}
- run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }}
- uses: XAMPPRocky/create-release@v1.0.2
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- uses: actions/upload-release-asset@v1
id: upload-release-asset
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.BIN }}.tar.gz
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.tar.gz
asset_content_type: application/gzip
linux:
runs-on: ubuntu-latest
needs: install-cross
strategy:
matrix:
target:
- aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- arm-unknown-linux-gnueabi
- arm-unknown-linux-gnueabihf
- armv7-unknown-linux-gnueabihf
- i686-unknown-linux-musl
- x86_64-unknown-linux-musl
- x86_64-unknown-linux-gnu
steps:
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- uses: actions/checkout@v2
- uses: actions/download-artifact@v4
with:
name: cross-linux-musl
path: /tmp/
- run: chmod +x /tmp/cross
- run: ci/set_rust_version.bash stable ${{ matrix.target }}
- run: ci/build.bash /tmp/cross ${{ matrix.target }} RELEASE
- run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }}
- uses: XAMPPRocky/create-release@v1.0.2
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.BIN }}.tar.gz
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.tar.gz
asset_content_type: application/gzip
win:
runs-on: windows-latest
strategy:
matrix:
target:
- x86_64-pc-windows-msvc
steps:
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- uses: actions/checkout@v2
- run: rustup.exe default
- run: rustup.exe target add ${{ matrix.target }}
- name: Build Release
run: cargo.exe build --target ${{ matrix.target }} --release
- name: Zip Binary
run: 7z a ${{ env.BIN }}.zip ./target/${{ matrix.target }}/release/${{ env.BIN }}.exe
- uses: XAMPPRocky/create-release@v1.0.2
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.BIN }}.zip
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.zip
asset_content_type: application/gzip
================================================
FILE: .github/workflows/rustfmt.yml
================================================
# When pushed to master, run `cargo +nightly fmt --all` and open a PR.
name: rustfmt
on: [push, pull_request]
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install stable toolchain with rustfmt available
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt
- run: cargo fmt --check
================================================
FILE: .gitignore
================================================
.idea
/target
**/*.rs.bk
update.sh
.zsh_history
.zshrc
/.fish
.vscode/**/*
================================================
FILE: .travis.yml
================================================
# Based on the "trust" template v0.1.2
# https://github.com/japaric/trust/tree/v0.1.2
dist: trusty
language: rust
services: docker
sudo: required
env:
global:
- CRATE_NAME=mcfly
matrix:
include:
# Linux
# - env: TARGET=aarch64-unknown-linux-gnu
- env: TARGET=arm-unknown-linux-gnueabi
- env: TARGET=armv7-unknown-linux-gnueabihf
- env: TARGET=i686-unknown-linux-gnu
# - env: TARGET=i686-unknown-linux-musl
# - env: TARGET=mips-unknown-linux-gnu
# - env: TARGET=mips64-unknown-linux-gnuabi64
# - env: TARGET=mips64el-unknown-linux-gnuabi64
# - env: TARGET=mipsel-unknown-linux-gnu
# - env: TARGET=powerpc-unknown-linux-gnu
# - env: TARGET=powerpc64-unknown-linux-gnu
# - env: TARGET=powerpc64le-unknown-linux-gnu
# - env: TARGET=s390x-unknown-linux-gnu DISABLE_TESTS=1
- env: TARGET=x86_64-unknown-linux-gnu
# - env: TARGET=x86_64-unknown-linux-musl
# OSX
# - env: TARGET=i686-apple-darwin
# os: osx
- env: TARGET=x86_64-apple-darwin
os: osx
# *BSD
# - env: TARGET=i686-unknown-freebsd DISABLE_TESTS=1
# - env: TARGET=x86_64-unknown-freebsd DISABLE_TESTS=1
# - env: TARGET=x86_64-unknown-netbsd DISABLE_TESTS=1
# Windows
# - env: TARGET=x86_64-pc-windows-gnu
# Bare metal
# These targets don't support std and as such are likely not suitable for
# most crates.
# - env: TARGET=thumbv6m-none-eabi
# - env: TARGET=thumbv7em-none-eabi
# - env: TARGET=thumbv7em-none-eabihf
# - env: TARGET=thumbv7m-none-eabi
# Testing other channels
- env: TARGET=x86_64-unknown-linux-gnu
rust: nightly
- env: TARGET=x86_64-apple-darwin
os: osx
rust: nightly
before_install:
- set -e
- rustup self update
install:
- sh ci/install.sh
- source ~/.cargo/env || true
script:
- bash ci/script.sh
after_script: set +e
before_deploy:
- sh ci/before_deploy.sh
deploy:
# - Create a `public_repo` GitHub token. Go to: https://github.com/settings/tokens/new
# - Encrypt it: `travis encrypt 0123456789012345678901234567890123456789`
# - Note: if your project is on travis-ci.com instead of travis-ci.org you
# need to add the `--pro` flag like:
# `travis encrypt 0123456789012345678901234567890123456789 --pro`
# - Paste the output down here
api_key:
secure: "1rJFIUde5cmrrkrjBkmSFgJ3U1jRl0lf+tm0fkxfaTbaYvM1HeTNUquWKCicFthRul+TihytnIlmeCA0YhSqA+kqJps9Qw3WV9U9kLLpw/nNM1ms1x+SmSP3TetIhNftsOBpVgoX9qS46uQoljZGouU/B3a+1CIaCG+nuQL0MiB4pXi3TrQUnFtaA696NDxZxC3IR+CmTpjgJ+Q2/1oEBUW4cbYJAs8nQe+GZGf2Ijh+gLb2Qtesimf8HG1hxhfPEFq+oPlOiR3EAnFPP4QBBgSO/qo3zp4vPei+pBd3OGuWnPn3KbTNeCD4aojtjKBQQa0VES8CRISZOBwNE4Rl2w1unf4aHmXM/H2Tzy+185a3X1bilv+S6sjJlkAmvsZzdD5H9gBCBLPW0fJpNdAjR7cSaoHU3sNKXuW7pulDselz9I6fAO5Y9avBf0NbrknM3p/Fy5qn2aIQyAt4U7rswgA3ydsgHKclwTZKm6+ub+BB90gRRCX9ugods9zTre0TVxGwgz1fiukfNM1+wNpdz4jpthHi99GF7kzb8hOyHZQo5vx/mBX8kWbvZ3oQO9VA+oBs9rZnabBnJ6Mv/liPGdlbG19dxSSFzig6pza6zkWmi9UC6Ghwh6oQ/mYfaT3+0Tlql0qh0xXG9ogHhTxiexlLlFqFomp4ER+vkjNQXnE="
file_glob: true
file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.*
on:
# Here you can pick which targets will generate binary releases
# In this example, there are some targets that are tested using the stable
# and nightly channels. This condition makes sure there is only one release
# for such targets and that's generated using the stable channel
condition: $TRAVIS_RUST_VERSION = stable
tags: true
provider: releases
prerelease: true
skip_cleanup: true
cache: cargo
before_cache:
# Travis can't cache files that are not readable by "others"
- chmod -R a+r $HOME/.cargo
branches:
only:
# release tags
- /^v\d+\.\d+\.\d+.*$/
- master
notifications:
email:
on_success: never
================================================
FILE: CHANGELOG.txt
================================================
0.9.4 - Dec 24, 2025
- Implement case sensitivity for inputs containing uppercase letters, highlighing only matched characters (thanks @kentakom1213)
- Fish fixes (thanks @praveenperera!)
- Make the result selection in dark mode easier to see (thanks @cjordan)
- Upgrade Rust to 2024 edition
0.9.3 - Feb 11, 2025
- Fix PowerShell VIM key scheme duplicates keypresses (thanks @jtschuster)
- Display more results by default (thanks @kentakom1213) and scrolling (thanks @kentakom1213)
- Hopefully fix occasional sort panics (thanks @unexge)
0.9.2 - Aug 11, 2024
- Numerous bash fixes (thanks @akinomyoga)
- Bind to both emacs and vi-insert keymaps in Bash (thanks @akinomyoga)
0.9.1 - July 10, 2024
- Added the `mcfly stats` command (thanks @nicokosi)
- Prevent cancellation of Bash/Zsh initialization on mcfly initialization failure (thanks @akinomyoga)
- Prevent UTF-8 entry issue from new default to avoid TIOCSTI added in 0.9.0 (thanks @akinomyoga)
0.9.0 - May 31, 2024
- Make bash no longer use TIOCSTI by default (thanks @jtschuster)
0.8.6 - May 18, 2024
- Add windows asset generation (thanks @jtschuster)
- Fix init issue for fish (thanks @4t8dd)
0.8.5 - May 11, 2024
- Use C:\Users\username\AppData\Local instead of C:\Users\username\AppData\Roaming (thanks @jtschuster)
- Fix Fish return codes (thanks @manfredlotz)
- Allow colors to be configured in a new optional config.toml (thanks @exokernel)
- Paste in emacs mode (thanks @eatradish)
0.8.4 - Dec 24, 2023
- Remove spurious print when moving files.
- PowerShell improvements (thanks @Namorzyny and @YoshidaRyoko!)
0.8.3 - Dec 3, 2023
- Add support for exporting command history matching a regex or date range (thanks @TD-Sky!)
- Add Windows and Powershell support (thanks @jtschuster!)
- Add deprecation warning on brew tap.
0.8.1 - Jun 3, 2023
- Fix use of MCFLY_DISABLE_MENU (thanks @barklan!)
- Support Fish private mode support (thanks @Aeron!)
- Always set MCFLY_HISTORY in zsh to support subshells (thanks @utkarshgupta137!)
- Allow linking mcfly with system-provided sqlite with sqlite-bundled feature flag (thanks @jirutka!)
- And allow sourcing in zsh more than once (thanks @ahatzz11 and @deekshithanand!)
- Reduce size by removing unnecessary/unused regex features (thanks @jirutka!)
- Add per-directory history (thanks @rawkode!)
0.8.0 - Mar 6, 2023
- Add forward-compatibility check for database schema (thanks @bnprks!)
- Add MCFLY_DISABLE_RUN_COMMAND option to disable command running (thanks @chaserhkj!)
- Add customizable prompt with MCFLY_PROMPT (thanks @vedkothavade!)
- Replace termion with crossterm (big change, thanks @jtschuster!)
- Allow ENV variables to be set to FALSE
0.7.1 - Dec 15, 2022
- Ensure at least MCFLY_HISTFILE is set for history import when HISTFILE is missing
0.7.0 - Dec 10, 2022
- Upgraded to clap4 (thanks @TD-Sky!)
- Switched back to which for command location due to issues when run at root (thanks @Efreak and @joefiorini!)
- Stopped exporting HISTFILE to fix issue when using nested shells (thanks @dithpri and @AndrewKvalheim!)
- Added ctrl-w to vim keybinds (thanks @copy!)
- Cursor no longer jumps to top when deleting history (thanks @navazjm!)
- Fixed compatibility with mktemp from uutils/coreutils (thanks @jhult!)
- Skip fuzzy matches when sorting by time (thanks @navazjm!)
- Fix handling of open-quote strings in fish (thanks @hivehand!)
0.6.1 - Jul 16, 2022
- Avoid return 0 to prevent re-sourcing .zshrc from erroring
- Vim mode improvement (thanks @fabiogibson!)
- Allow switching between rank and time-based sorting with F1 (thanks @navazjm!)
- Dependency security updates
0.6.0 - Mar 22, 2022
- Allow disabling of menu (thanks @michaelnavs!)
- Prevent subshells from having multiple mcfly PROMPT_COMMAND hooks in bash (thanks @nfultz!)
- Errors during history import do not prevent other lines from importing (thanks @qouoq!)
- Store configuration in XDG directories when ~/.mcfly does not yet exist (thanks @Awernx!)
0.5.13 - Jan 24, 2022
- Fix 'illegal byte sequence' due to incorrect TIOCSTI cast (thanks @arunpersaud!)
0.5.12 - Jan 12, 2022
- Automatically detect if Zsh extended history is used (thanks @vmax!)
0.5.11 - Dec 12, 2021
- Avoid using builtins to fix WSL bug
0.5.10 - Nov 6, 2021
- Fix zsh utf-8 history encoding (thanks @onriv!)
- Support Ctrl-p and Ctrl-n in vim mode (thanks @otherJL0!)
- Make MCFLY_FUZZY a tuneable int (thanks @dmfay!)
- Prevent errors when running bash inside of fish (thanks @btglr!)
0.5.9 - Aug 29, 2021
- Prefer unaliased commands in bash/zsh (thanks @Mic92!)
- Fix zsh source message (thanks @hlascelles!)
- Prevent potentially unsafe variable substitution in paths (thanks @CreativeCactus!)
0.5.8 - Aug 1, 2021
- Option to place interface at bottom of screen (thanks @agrism)
- Option to sort by recency (thanks @agrism)
- Option to skip prompting on command deletion (thanks @goddade)
0.5.7 - Jun 27, 2021
- Document MCFLY_RESULTS config value
- Initialize database inside a transaction for speed (thanks @SafariMonkey!)
- Move to cantino/homebrew-mcfly for tap install
- Show run time of commands (thanks @dmfay!)
- Clean PROMPT_COMMAND before joining with a semicolon in Bash
- Fix zsh interactivity test
- Make ^d delete forward (thanks @rbutoi!)
- Move to Github Actions for build and add install script instructions (thanks @praveenperera!)
0.5.6 - Apr 1, 2021
- Fix fish shell initialization (thanks @domoritz)
- Fix fish shell escaping (thanks @scooter-dangle!)
0.5.5 - Mar 12, 2021
- Fixed a crash during init without any history
- Fixed a crash during init without any history
- Fixed issue when deleting all history (thanks @akinnane!)
- Add MCFLY_HISTORY_LIMIT to limit history search
0.5.4 - Feb 28, 2021
- Switched to `mcfly init` pattern for shell config files (thanks @b3nj5m1n!)
0.5.3 - Jan 17, 2021
- Ensure that history is appended in Bash 4+.
0.5.2 - Dec 10, 2020
- Bash 4+ should no longer have a cluttered terminal buffer (thanks @CreativeCactus)
- Vim mode now starts in insert mode (thanks @JamJar00)
0.5.1 - Dec 6, 2020
- Fuzzy searching via the MCFLY_FUZZY option from @dmfay.
0.5.0 - Aug 21, 2020
- Fish support! Thanks @tjkirch!
0.4.0 - Jun 28, 2020
- Zsh support!
0.3.6 - Dec 15, 2019
- Optional VI-style keybindings from @JamJar00
0.3.5 - Aug 30, 2019
- Remake the history file if it gets removed
0.3.4 - May 24, 2019
- Only read 256 bytes for session id generation (thanks @SuperSandro2000!)
- Prevent adding empty commands
- Try using unlock_notify to prevent race condition with locked DB.
- Ensure stdin is a tty to fix issue with Sublime Text 3 (thanks @abuzze!)
0.3.3 - Feb 11, 2019
- Fix version number
0.3.2 - Feb 10, 2019
- Fix 'cat /dev/urandom' not closing (thanks @Melkor333!)
- Update to Rust 2018
- Error gracefully when .bash_history is not found
- Add more Xes for Slackware Linux (thanks @aik099)
0.3.1 - Dec 25, 2018
- Fix background color on Light Mode
0.3.0 - Dec 25, 2018
- Support users who have `set -o vi` (thanks @Asdalo21)
- Remove Regex dependency for a smaller binary.
- Add support for Light Mode - enable with `export MCFLY_LIGHT=TRUE` (thanks @mshron)
- Fix broken Rust install link (thanks @bperel)
0.2.5 - Dec 9, 2018
- Prevent clobbering of command return statuses (thanks @gwk)
- Add Ctrl-n and Ctrl-p mappings (thanks @greyblake)
- Support spaces in HISTFILE (thanks @markusjevringgoeuro)
0.2.4 - Dec 4, 2018
- Important update: fixes bug where historical directory paths would be incorrectly updated when a directory that was a
prefix of another was moved, resulting in historical directory references that never actually existed.
- Silences logs when moving / renaming directories.
- Fixes importing of shell history that contains invalid UTF8 characters.
0.2.3 - Dec 3, 2018
- Note: 0.2.3 was never built as a release or pushed to Homebrew.
- Use clobbering redirects, thanks to @gwk.
================================================
FILE: Cargo.toml
================================================
[package]
name = "mcfly"
version = "0.9.4"
authors = ["Andrew Cantino <cantino@users.noreply.github.com>"]
edition = "2024"
description = "McFly replaces your default ctrl-r shell history search with an intelligent search engine that takes into account your working directory and the context of recently executed commands. McFly's suggestions are prioritized in real time with a small neural network."
license = "MIT"
repository = "https://github.com/cantino/mcfly"
categories = ["command-line-utilities"]
exclude = ["HomebrewFormula", "HomebrewFormula/*", "pkg/*", "docs/*"]
[profile.release]
lto = true
[profile.dev]
debug = true
[dependencies]
config = { version = "0.15", default-features = false, features = ["toml"] }
chrono = "0.4"
chrono-systemd-time = "0.3"
csv = "1"
serde_json = "1"
serde = { version = "1", features = ["derive"] }
humantime = "2.1"
directories-next = "2.0"
itertools = "0.14"
rand = "0.9"
path-absolutize = "3.1"
regex = { version = "1", default-features = false, features = ["perf", "std"] }
shellexpand = "3"
unicode-segmentation = "1.11"
[dependencies.rusqlite]
version = "0.38"
features = ["functions", "unlock_notify"]
[dependencies.crossterm]
version = "0.28"
features = ["use-dev-tty"]
[dependencies.clap]
version = "4"
features = ["derive"]
[target.'cfg(not(windows))'.dependencies]
libc = "0.2"
[target.'cfg(windows)'.dependencies]
autopilot = "0.4.0"
[features]
default = ["sqlite-bundled"]
sqlite-bundled = ["rusqlite/bundled"]
================================================
FILE: LICENSE
================================================
The MIT License
Copyright (c) 2018, Andrew Cantino
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
> **Seeking co-maintainers**:
> I don't have much time to maintain this project these days. If someone would like to jump in and become a co-maintainer, it would be appreciated!

[](https://crates.io/crates/mcfly)
# McFly - fly through your shell history
<img src="/docs/screenshot.png" alt="screenshot" width="400">
McFly replaces your default `ctrl-r` shell history search with an intelligent search engine that takes into account
your working directory and the context of recently executed commands. McFly's suggestions are prioritized
in real time with a small neural network.
TL;DR: an upgraded `ctrl-r` where history results make sense for what you're working on right now.
## Features
* Rebinds `ctrl-r` to bring up a full-screen reverse history search prioritized with a small neural network.
* Augments your shell history to track command exit status, timestamp, and execution directory in a SQLite database.
* Maintains your normal shell history file as well so that you can stop using McFly whenever you want.
* Unicode support throughout.
* Includes a simple action to scrub any history item from the McFly database and your shell history files.
* Designed to be extensible for other shells in the future.
* Written in Rust, so it's fast and safe.
* You can type `%` to match any number of characters when searching.
* Supports Zsh, Bash (version 3+), and PowerShell (version 7+)
## Prioritization
The key feature of McFly is smart command prioritization powered by a small neural network that runs
in real time. The goal is for the command you want to run to always be one of the top suggestions.
When suggesting a command, McFly takes into consideration:
* The directory where you ran the command. You're likely to run that command in the same directory in the future.
* What commands you typed before the command (e.g., the command's execution context).
* How often you run the command.
* When you last ran the command.
* If you've selected the command in McFly before.
* The command's historical exit status. You probably don't want to run old failed commands.
## Installation
### Install with Homebrew (on macOS or Linux)
1. Install `mcfly`:
```bash
brew install mcfly
```
1. Add the following to the end of your `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish` file:
Bash:
```bash
eval "$(mcfly init bash)"
```
Zsh:
```bash
eval "$(mcfly init zsh)"
```
Fish:
```bash
mcfly init fish | source
```
1. Run `. ~/.bashrc` / `. ~/.zshrc` / `source ~/.config/fish/config.fish` or restart your terminal emulator.
#### Uninstalling with Homebrew
1. Remove `mcfly`:
```bash
brew uninstall mcfly
```
1. Remove the lines you added to `~/.bashrc` / `~/.zshrc` / `~/.config/fish/config.fish`.
### Install with MacPorts (on macOS)
1. Update the ports tree
```bash
sudo port selfupdate
```
1. Install `mcfly`:
```bash
sudo port install mcfly
```
1. Add the following to the end of your `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish` file, as appropriate:
Bash:
```bash
eval "$(mcfly init bash)"
```
Zsh:
```bash
eval "$(mcfly init zsh)"
```
Fish:
```bash
mcfly init fish | source
```
1. Run `. ~/.bashrc` / `. ~/.zshrc` / `source ~/.config/fish/config.fish` or restart your terminal emulator.
#### Uninstalling with MacPorts
1. Remove `mcfly`:
```bash
sudo port uninstall mcfly
```
1. Remove the lines you added to `~/.bashrc` / `~/.zshrc` / `~/.config/fish/config.fish`.
### Install using WinGet on Windows (we do not maintain this install script and cannot vouch for its accuracy or safety)
1. Install `mcfly`:
```shell
winget install mcfly
```
2. Add the following to the end of your `$PROFILE`:
```shell
Invoke-Expression -Command $(mcfly init powershell | out string)
```
3. Restart your terminal
#### Uninstall using Winget
1. Remove `mcfly`:
```shell
winget uninstall mcfly
```
2. Remove the lines you added to `$PROFILE`.
### Installing using our install script (macOS or Linux)
1. `curl -LSfs https://raw.githubusercontent.com/cantino/mcfly/master/ci/install.sh | sh -s -- --git cantino/mcfly` (or, if the current user doesn't have permissions to edit /usr/local/bin, then use `sudo sh -s`.)
2. Add the following to the end of your `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish` file, respectively:
Bash:
```bash
eval "$(mcfly init bash)"
```
Zsh:
```bash
eval "$(mcfly init zsh)"
```
Fish:
```bash
mcfly init fish | source
```
3. Run `. ~/.bashrc` / `. ~/.zshrc` / `source ~/.config/fish/config.fish` or restart your terminal emulator.
### Installing manually from GitHub (macOS or Linux)
1. Download the [latest release from GitHub](https://github.com/cantino/mcfly/releases).
1. Install to a location in your `$PATH`. (For example, you could create a directory at `~/bin`, copy `mcfly` to this location, and add `export PATH="$PATH:$HOME/bin"` to your `.bashrc` / `.zshrc`, or run `set -Ua fish_user_paths "$HOME/bin"` for fish.)
1. Add the following to the end of your `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish`, respectively:
Bash:
```bash
eval "$(mcfly init bash)"
```
Zsh:
```bash
eval "$(mcfly init zsh)"
```
Fish:
```bash
mcfly init fish | source
```
1. Run `. ~/.bashrc` / `. ~/.zshrc` / `source ~/.config/fish/config.fish` or restart your terminal emulator.
### Install manually from source (macOS, Linux, or Windows)
1. [Install Rust 1.40 or later](https://www.rust-lang.org/tools/install)
1. Run `git clone https://github.com/cantino/mcfly` and `cd mcfly`
1. Run `cargo install --path .`
1. Ensure `~/.cargo/bin` is in your `$PATH`.
1. Add the following to the end of your `~/.bashrc`, `~/.zshrc`, `~/.config/fish/config.fish`, or powershell `$PROFILE`, respectively:
Bash:
```bash
eval "$(mcfly init bash)"
```
Zsh:
```bash
eval "$(mcfly init zsh)"
```
Fish:
```bash
mcfly init fish | source
```
Powershell Core (pwsh)
```powershell
Invoke-Expression -Command $(mcfly init powershell | out-string)
```
1. Run `. ~/.bashrc` / `. ~/.zshrc` / `source ~/.config/fish/config.fish` / `. $PROFILE` or restart your terminal emulator.
### Install by [Zinit](https://github.com/zdharma-continuum/zinit)
* Add below code to your zshrc.
```zsh
zinit ice lucid wait"0a" from"gh-r" as"program" atload'eval "$(mcfly init zsh)"'
zinit light cantino/mcfly
```
* It will download mcfly and install for you.
* `$(mcfly init zsh)` will be executed after prompt
## iTerm2
To avoid McFly's UI messing up your scrollback history in iTerm2, make sure this option is unchecked:
<img src="/docs/iterm2.jpeg" alt="iterm2 UI instructions">
## Dump history
McFly can dump the command history into *stdout*.
For example:
```bash
mcfly dump --since '2023-01-01' --before '2023-09-12 09:15:30'
```
will dump the command run between *2023-01-01 00:00:00.0* to *2023-09-12 09:15:30*(**exclusive**) as **json**.
You can specify **csv** as dump format via `--format csv` as well.
Each item in dumped commands has the following fields:
* `cmd`: The run command.
* `when_run`: The time when the command ran in your local timezone.
You can dump all the commands history without any arguments:
```bash
mcfly dump
```
### Timestamp format
McFly parses timestamps via `chrono-systemd-time`, a non-strict implementation of [systemd.time](https://www.freedesktop.org/software/systemd/man/systemd.time.html), with the following exceptions:
* time units **must** accompany all time span values.
* time zone suffixes are **not** supported.
* weekday prefixes are **not** supported.
McFly users simply need to understand **specifying timezone in timestamp isn't allowed**.
McFly will always use your **local timezone**.
For more details, please refer to the [`chrono-systemd-time` documentation](https://docs.rs/chrono-systemd-time/latest/chrono_systemd_time/).
### Regex
*Dump* supports filtering commands with regex.
The regex syntax follows [crate regex](https://docs.rs/regex/latest/regex/#syntax).
For example:
```bash
mcfly dump -r '^cargo run'
```
will dump all command prefixes with `cargo run`.
You can use `-r/--regex` and time options at the same time.
For example:
```bash
mcfly dump -r '^cargo run' --since '2023-09-12 09:15:30'
```
will dump all command prefixes with `cargo run` ran since *2023-09-12 09:15:30*.
## Settings
A number of settings can be set via environment variables. To set a setting you should add the following snippets to your `~/.bashrc` / `~/.zshrc` / `~/.config/fish/config.fish`.
[Color settings](https://github.com/cantino/mcfly/blob/b54adb65e1567887fe430188324c09553431eb7c/src/settings.rs#L508) can be set in a config file, which will be in `~/.mcfly` if it exists, otherwise in `$XDG_DATA_DIR/mcfly`. On MacOS, this would be `~/Library/Application Support/McFly/config.toml`.
### Light Mode
To swap the color scheme for use in a light terminal, set the environment variable `MCFLY_LIGHT`.
bash / zsh:
```bash
export MCFLY_LIGHT=TRUE
```
fish:
```bash
set -gx MCFLY_LIGHT TRUE
```
powershell:
```powershell
$env:MCFLY_LIGHT = "TRUE"
```
Tip: on macOS you can use the following snippet for color scheme to be configured based on system-wide settings:
bash / zsh:
```bash
if [[ "$(defaults read -g AppleInterfaceStyle 2&>/dev/null)" != "Dark" ]]; then
export MCFLY_LIGHT=TRUE
fi
```
### VIM Key Scheme
By default Mcfly uses an `emacs` inspired key scheme. If you would like to switch to the `vim` inspired key scheme, set the environment variable `MCFLY_KEY_SCHEME`.
bash / zsh:
```bash
export MCFLY_KEY_SCHEME=vim
```
fish:
```bash
set -gx MCFLY_KEY_SCHEME vim
```
powershell:
```powershell
$env:MCFLY_KEY_SCHEME="vim"
```
### Fuzzy Searching
To enable fuzzy searching, set `MCFLY_FUZZY` to an integer. 0 is off; higher numbers weight toward shorter matches. Values in the 2-5 range get good results so far; try a few and [report what works best for you](https://github.com/cantino/mcfly/issues/183)!
bash / zsh:
```bash
export MCFLY_FUZZY=2
```
fish:
```bash
set -gx MCFLY_FUZZY 2
```
powershell:
```powershell
$env:MCFLY_FUZZY=2
```
### Results Count
To change the maximum number of results shown, set `MCFLY_RESULTS` (default: 30).
bash / zsh:
```bash
export MCFLY_RESULTS=50
```
fish:
```bash
set -gx MCFLY_RESULTS 50
```
powershell:
```powershell
$env:MCFLY_RESULTS=50
```
### Delete without confirmation
To delete without confirmation, set `MCFLY_DELETE_WITHOUT_CONFIRM` to true.
bash / zsh:
```bash
export MCFLY_DELETE_WITHOUT_CONFIRM=true
```
fish:
```bash
set -gx MCFLY_DELETE_WITHOUT_CONFIRM true
```
powershell:
```powershell
$env:MCFLY_DELETE_WITHOUT_CONFIRM="true"
```
### Interface view
To change interface view, set `MCFLY_INTERFACE_VIEW` (default: `TOP`).
Available options: `TOP` and `BOTTOM`
bash / zsh:
```bash
export MCFLY_INTERFACE_VIEW=BOTTOM
```
fish:
```bash
set -gx MCFLY_INTERFACE_VIEW BOTTOM
```
powershell:
```powershell
$env:MCFLY_INTERFACE_VIEW="BOTTOM"
```
### Disable menu interface
To disable the menu interface, set the environment variable `MCFLY_DISABLE_MENU`.
bash / zsh:
```bash
export MCFLY_DISABLE_MENU=TRUE
```
fish:
```bash
set -gx MCFLY_DISABLE_MENU TRUE
```
powershell:
```powershell
$env:MCFLY_DISABLE_MENU=true
```
### Results sorting
To change the sorting of results shown, set `MCFLY_RESULTS_SORT` (default: RANK).
Possible values `RANK` and `LAST_RUN`
bash / zsh:
```bash
export MCFLY_RESULTS_SORT=LAST_RUN
```
fish:
```bash
set -gx MCFLY_RESULTS_SORT LAST_RUN
```
powershell:
```powershell
$env:MCFLY_RESULTS_SORT="LAST_RUN"
```
### Custom Prompt
To change the prompt, set `MCFLY_PROMPT` (default: `$`).
bash / zsh:
```bash
export MCFLY_PROMPT="❯"
```
fish:
```bash
set -gx MCFLY_PROMPT "❯"
```
powershell:
```powershell
$env:MCFLY_PROMPT=">"
```
Note that only single-character-prompts are allowed. setting `MCFLY_PROMPT` to `"<str>"` will reset it to the default prompt.
### Database Location
McFly stores its SQLite database in the standard location for the OS. On OS X, this is in `~/Library/Application Support/McFly`, on Linux it is in `$XDG_DATA_DIR/mcfly/history.db` (default would be `~/.local/share/mcfly/history.db`), and on Windows, it is `%LOCALAPPDATA%\McFly\data\history.db`. For legacy support, if `~/.mcfly/` exists, it is used instead.
### Slow startup
If you have a very large history database and you notice that McFly launches slowly, you can set `MCFLY_HISTORY_LIMIT` to something like 10000 to limit how many records are considered when searching. In this example, McFly would search only the latest 10,000 entries.
### Bash TIOCSTI
Starting with Linux kernel version 6.2, some systems have disabled TIOCSTI (which McFly previously used to write the selected command). McFly works around this issue by using two "dummy" keybindings, which default to `ctrl-x 1` and `ctrl-x 2`. If you are using either of these for another purpose, you can set the `MCFLY_BASH_SEARCH_KEYBINDING` and `MCFLY_BASH_ACCEPT_LINE_KEYBINDING`, respectively, to something you are not using. If you would prefer to use the legacy TIOCSTI behavior, you can enable it by setting the `sysctl` variable `dev.tty.legacy_tiocsti` to `1` on your system and set the `MCFLY_BASH_USE_TIOCSTI` bash variable to `1`.
## HISTTIMEFORMAT
McFly currently doesn't parse or use `HISTTIMEFORMAT`.
## Possible Future Features
* Add a screencast to README.
* Learn common command options and autocomplete them in the suggestion UI?
* Sort command line args when coming up with the template matching string.
* Possible prioritization improvements:
* Cross validation & explicit training set selection.
* Learn command embeddings
## Development
### Contributing
Contributions and bug fixes are encouraged! However, we may not merge PRs that increase complexity significantly beyond what is already required to maintain the project. If you're in doubt, feel free to open an issue and ask.
### Running tests
`cargo test`
### Releasing (notes for @cantino)
1. Edit `Cargo.toml` and bump the version.
1. Edit CHANGELOG.txt
1. Run `cargo clippy` and `cargo fmt`.
1. Recompile (`cargo build`) and test (`cargo test`)
1. `git add -p`
1. `git ci -m 'Bumping to vx.x.x'`
1. `git tag vx.x.x`
1. `git push origin head --tags`
1. Let the build finish.
1. Edit the new Release on Github.
1. `cargo publish`
1. TBD: update homebrew-core Formula at https://github.com/Homebrew/homebrew-core/blob/master/Formula/m/mcfly.rb
Old:
1. Edit `pkg/brew/mcfly.rb` and update the version and SHAs. (`shasum -a 256 ...`)
1. Edit `../homebrew-mcfly/pkg/brew/mcfly.rb` too.
1. `cp pkg/brew/mcfly.rb ../homebrew-mcfly/pkg/brew/mcfly.rb`
1. Compare with `diff ../homebrew-mcfly/pkg/brew/mcfly.rb ../mcfly/pkg/brew/mcfly.rb ; diff ../homebrew-mcfly/HomebrewFormula/mcfly.rb ../mcfly/HomebrewFormula/mcfly.rb`
1. `git add -p && git ci -m 'Update homebrew' && git push`
1. `cd ../homebrew-mcfly && git add -p && git ci -m 'Update homebrew' && git push && cd ../mcfly`
================================================
FILE: ci/before_deploy.sh
================================================
# This script takes care of building your crate and packaging it for release
set -ex
main() {
local src=$(pwd) \
stage=
case $TRAVIS_OS_NAME in
linux)
stage=$(mktemp -d)
;;
osx)
stage=$(mktemp -d -t tmp)
;;
esac
test -f Cargo.lock || cargo generate-lockfile
cross rustc --bin mcfly --target $TARGET --release -- -C lto
# strip target/$TARGET/release/mcfly
cp target/$TARGET/release/mcfly $stage/
cp mcfly.bash $stage/
cp mcfly.zsh $stage/
cp mcfly.fish $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
cd $src
rm -rf $stage
}
main
================================================
FILE: ci/build.bash
================================================
#!/usr/bin/env bash
# Script for building your rust projects.
set -e
source ci/common.bash
# $1 {path} = Path to cross/cargo executable
CROSS=$1
# $1 {string} = <Target Triple> e.g. x86_64-pc-windows-msvc
TARGET_TRIPLE=$2
# $3 {boolean} = Are we building for deployment?
RELEASE_BUILD=$3
required_arg $CROSS 'CROSS'
required_arg $TARGET_TRIPLE '<Target Triple>'
if [ -z "$RELEASE_BUILD" ]; then
$CROSS build --target $TARGET_TRIPLE
$CROSS build --target $TARGET_TRIPLE --all-features
else
$CROSS build --target $TARGET_TRIPLE --all-features --release
fi
================================================
FILE: ci/common.bash
================================================
required_arg() {
if [ -z "$1" ]; then
echo "Required argument $2 missing"
exit 1
fi
}
================================================
FILE: ci/install.sh
================================================
#!/bin/sh
# Heavily modified from https://github.com/japaric/trust/blob/gh-pages/install.sh.
help() {
cat <<'EOF'
Install a binary release of a Rust crate hosted on GitHub.
Usage:
install.sh [options]
Options:
-h, --help Display this message
--git SLUG Get the crate from "https://github/$SLUG"
-f, --force Force overwriting an existing binary
--crate NAME Name of the crate to install (default <repository name>)
--tag TAG Tag (version) of the crate to install (default <latest release>)
--to LOCATION Where to install the binary (default /usr/local/bin)
EOF
}
say() {
echo "install.sh: $1"
}
say_err() {
say "$1" >&2
}
err() {
if [ -n "$td" ]; then
rm -rf "$td"
fi
say_err "ERROR $1"
exit 1
}
need() {
if ! command -v "$1" > /dev/null 2>&1; then
err "need $1 (command not found)"
fi
}
force=false
while test $# -gt 0; do
case $1 in
--crate)
crate=$2
shift
;;
--force | -f)
force=true
;;
--git)
git=$2
shift
;;
--help | -h)
help
exit 0
;;
--tag)
tag=$2
shift
;;
--to)
dest=$2
shift
;;
*)
;;
esac
shift
done
# Dependencies
need basename
need curl
need install
need mkdir
need mktemp
need tar
# Optional dependencies
if [ -z "$crate" ] || [ -z "$tag" ] || [ -z "$target" ]; then
need cut
fi
if [ -z "$tag" ]; then
need grep
fi
if [ -z "$git" ]; then
# shellcheck disable=SC2016
err 'must specify a git repository using `--git`. Example: `install.sh --git japaric/cross`'
fi
url="https://github.com/$git"
if [ "$(curl --head --write-out "%{http_code}\n" --silent --output /dev/null "$url")" -eq "404" ]; then
err "GitHub repository $git does not exist"
fi
say_err "GitHub repository: $url"
if [ -z "$crate" ]; then
crate=$(echo "$git" | cut -d'/' -f2)
fi
say_err "Crate: $crate"
if [ -z "$dest" ]; then
dest="/usr/local/bin"
fi
if [ -e "$dest/$crate" ] && [ $force = false ]; then
err "$crate already exists in $dest, use --force to overwrite the existing binary"
fi
url="$url/releases"
if [ -z "$tag" ]; then
tag=$(curl "https://api.github.com/repos/$git/releases/latest" | grep "tag_name" | cut -d'"' -f4)
say_err "Tag: latest ($tag)"
else
say_err "Tag: $tag"
fi
case "$(uname -s)" in
"Darwin")
case "$(uname -m)" in
"x86_64")
target="x86_64-apple-darwin"
;;
"arm64")
## replace when M1 builds are working
# target="aarch64-apple-darwin"
target="x86_64-apple-darwin"
;;
esac
;;
"Linux")
platform="unknown-linux-musl"
target="$(uname -m)-$platform"
;;
esac
say_err "Target: $target"
url="$url/download/$tag/$crate-$tag-$target.tar.gz"
say_err "Downloading: $url"
if [ "$(curl --head --write-out "%{http_code}\n" --silent --output /dev/null "$url")" -eq "404" ]; then
err "$url does not exist, you will need to build $crate from source"
fi
td=$(mktemp -d || mktemp -d -t tmp)
curl -sL "$url" | tar -C "$td" -xz
say_err "Installing to: $dest"
for f in "$td"/*; do
[ -e "$f" ] || break # handle the case of no *.wav files
test -x "$f" || continue
if [ -e "$dest/$crate" ] && [ $force = false ]; then
err "$crate already exists in $dest"
else
mkdir -p "$dest"
cp "$f" "$dest"
chmod 0755 "$dest/$crate"
fi
done
rm -rf "$td"
================================================
FILE: ci/script.sh
================================================
# This script takes care of testing your crate
set -ex
main() {
cross build --target $TARGET
cross build --target $TARGET --release
if [ ! -z $DISABLE_TESTS ]; then
return
fi
cross test --target $TARGET
cross test --target $TARGET --release
# cross run --target $TARGET
# cross run --target $TARGET --release
}
# we don't run the "test phase" when doing deploys
if [ -z $TRAVIS_TAG ]; then
main
fi
================================================
FILE: ci/set_rust_version.bash
================================================
#!/usr/bin/env bash
set -e
rustup default $1
rustup target add $2
================================================
FILE: ci/test.bash
================================================
#!/usr/bin/env bash
# Script for building your rust projects.
set -e
source ci/common.bash
# $1 {path} = Path to cross/cargo executable
CROSS=$1
# $1 {string} = <Target Triple>
TARGET_TRIPLE=$2
required_arg $CROSS 'CROSS'
required_arg $TARGET_TRIPLE '<Target Triple>'
$CROSS test --target $TARGET_TRIPLE
$CROSS test --target $TARGET_TRIPLE --all-features
================================================
FILE: dev.bash
================================================
#!/bin/bash
# Build mcfly and run a dev environment bash for local mcfly testing
if ! this_dir=$(cd "$(dirname "$0")" && pwd); then
exit $?
fi
rm -f target/debug/mcfly
cargo build
# For some reason, to get line numbers in backtraces, we have to run the binary directly.
HISTFILE=$HOME/.bash_history \
MCFLY_PATH=target/debug/mcfly \
RUST_BACKTRACE=full \
MCFLY_DEBUG=1 \
PATH=target/debug/:$PATH \
exec /bin/bash --init-file "$this_dir/mcfly.bash" -i
================================================
FILE: dev.fish
================================================
#!/bin/bash
# Build mcfly and run a dev environment fish for local mcfly testing
this_dir=$(cd `dirname "$0"`; pwd)
# Setup for local testing.
mkdir -p $this_dir/.fish
rm -f target/debug/mcfly
cargo build
# For some reason, to get line numbers in backtraces, we have to run the binary directly.
XDG_DATA_HOME=$this_dir/.fish \
MCFLY_PATH=target/debug/mcfly \
RUST_BACKTRACE=full \
MCFLY_DEBUG=1 \
PATH=target/debug/:$PATH \
exec /usr/bin/env fish -i --init-command "source $this_dir/mcfly.fish; and mcfly_key_bindings"
================================================
FILE: dev.zsh
================================================
#!/bin/bash
# Build mcfly and run a dev environment zsh for local mcfly testing
this_dir=$(cd `dirname "$0"`; pwd)
# Setup for local testing.
touch $this_dir/.zsh_history
# Needed so that the test instance of zsh sources the local mcfly.zsh file on startup.
echo "source ./mcfly.zsh" > $this_dir/.zshrc
rm -f target/debug/mcfly
cargo build
# For some reason, to get line numbers in backtraces, we have to run the binary directly.
HISTFILE=$HOME/.zsh_history \
MCFLY_PATH=target/debug/mcfly \
RUST_BACKTRACE=full \
MCFLY_DEBUG=1 \
ZDOTDIR="$this_dir" \
PATH=target/debug/:$PATH \
/bin/zsh -i
================================================
FILE: mcfly.bash
================================================
#!/bin/bash
function mcfly_initialize {
# Note: We avoid using [[ ... ]] to check the Bash version because we are
# even unsure whether it is available before confirming the Bash version.
if [ -z "${BASH_VERSINFO-}" ] || [ "${BASH_VERSINFO-}" -lt 3 ]; then
printf 'mcfly.bash: This setup requires Bash >= 3.0.' >&2
return 1
fi
unset -f "${FUNCNAME[0]}"
# Ensure stdin is a tty
[[ -t 0 ]] || return 0
# Ensure an interactive shell
[[ $- =~ .*i.* ]] || return 0
# Avoid loading this file more than once
[[ ${__MCFLY_LOADED-} != "loaded" ]] || return 0
__MCFLY_LOADED="loaded"
# Setup MCFLY_HISTFILE and make sure it exists.
export MCFLY_HISTFILE="${HISTFILE:-$HOME/.bash_history}"
MCFLY_BASH_SEARCH_KEYBINDING=${MCFLY_BASH_SEARCH_KEYBINDING:-"\C-x1"}
MCFLY_BASH_ACCEPT_LINE_KEYBINDING=${MCFLY_BASH_ACCEPT_LINE_KEYBINDING:-"\C-x2"}
if [[ ! -r ${MCFLY_HISTFILE} ]]; then
echo "McFly: ${MCFLY_HISTFILE} does not exist or is not readable. Please fix this or set HISTFILE to something else before using McFly."
return 1
fi
# MCFLY_SESSION_ID is used by McFly internally to keep track of the commands from a particular terminal session.
MCFLY_SESSION_ID="$(command dd if=/dev/urandom bs=256 count=1 2> /dev/null | LC_ALL=C command tr -dc 'a-zA-Z0-9' | command head -c 24)"
export MCFLY_SESSION_ID
# Find the binary
MCFLY_PATH=${MCFLY_PATH:-$(builtin type -P mcfly)}
if [[ $MCFLY_PATH != /* ]]; then
# When the user include a relative path in PATH, "builtin type -P" may
# produce a relative path. We convert relative paths to the absolute ones.
MCFLY_PATH=$PWD/$MCFLY_PATH
fi
if [[ ! -x $MCFLY_PATH ]]; then
echo "Cannot find the mcfly binary, please make sure that mcfly is in your path before sourcing mcfly.bash."
return 1
fi
# Ignore commands with a leading space
export HISTCONTROL="${HISTCONTROL:-ignorespace}"
# Append new history items to .bash_history
shopt -s histappend
# Setup a function to be used by $PROMPT_COMMAND.
function mcfly_prompt_command {
local exit_code=$? # Record exit status of previous command.
# Populate McFly's temporary, per-session history file from recent commands in the shell's primary HISTFILE.
if [[ ! -f ${MCFLY_HISTORY-} ]]; then
MCFLY_HISTORY=$(command mktemp "${TMPDIR:-/tmp}"/mcfly.XXXXXXXX)
export MCFLY_HISTORY
command tail -n100 "${MCFLY_HISTFILE}" >| "${MCFLY_HISTORY}"
fi
history -a "${MCFLY_HISTORY}" # Append history to $MCFLY_HISTORY.
# Run mcfly with the saved code. It will:
# * append commands to $HISTFILE, (~/.bash_history by default)
# for backwards compatibility and to load in new terminal sessions;
# * find the text of the last command in $MCFLY_HISTORY and save it to the database.
"$MCFLY_PATH" add --exit "${exit_code}" --append-to-histfile "${MCFLY_HISTFILE}"
# Clear the in-memory history and reload it from $MCFLY_HISTORY
# (to remove instances of '#mcfly: ' from the local session history).
history -cr "${MCFLY_HISTORY}"
return "${exit_code}" # Restore the original exit code by returning it.
}
function mcfly_add_prompt_command {
local command=$1 IFS=$' \t\n'
if ((BASH_VERSINFO[0] > 5 || BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1)); then
# Bash 5.1 supports array PROMPT_COMMAND, where we register our prompt
# command to a new element PROMPT_COMMAND[i] (with i >= 1) to avoid
# conflicts with other frameworks.
if [[ " ${PROMPT_COMMAND[*]-} " != *" $command "* ]]; then
PROMPT_COMMAND[0]=${PROMPT_COMMAND[0]:-}
# Note: We here use eval to avoid syntax error in Bash < 3.1. We drop
# the support for Bash < 3.0, but this is still needed to avoid parse
# error before the Bash version check is performed.
eval 'PROMPT_COMMAND+=("$command")'
fi
elif [[ -z ${PROMPT_COMMAND-} ]]; then
PROMPT_COMMAND="$command"
elif [[ $PROMPT_COMMAND != *"mcfly_prompt_command"* ]]; then
PROMPT_COMMAND="$command;${PROMPT_COMMAND#;}"
fi
}
# Set $PROMPT_COMMAND run mcfly_prompt_command, preserving any existing $PROMPT_COMMAND.
mcfly_add_prompt_command "mcfly_prompt_command"
function mcfly_search_with_tiocsti {
local LAST_EXIT_CODE=$? IFS=$' \t\n'
echo "#mcfly: ${READLINE_LINE[*]}" >> "$MCFLY_HISTORY"
READLINE_LINE=
"$MCFLY_PATH" search
return "$LAST_EXIT_CODE"
}
# Runs mcfly search with output to file, reads the output, and sets READLINE_LINE to the command.
# If the command is to be run, binds the MCFLY_KEYSTROKE2 to accept-line, otherwise binds it to nothing.
function mcfly_search {
local LAST_EXIT_CODE=$? IFS=$' \t\n'
# Get a temp file name but don't create the file - mcfly will create the file for us.
local MCFLY_OUTPUT
MCFLY_OUTPUT=$(command mktemp --dry-run "${TMPDIR:-/tmp}"/mcfly.output.XXXXXXXX)
echo "#mcfly: ${READLINE_LINE[*]}" >> "$MCFLY_HISTORY"
"$MCFLY_PATH" search -o "$MCFLY_OUTPUT"
# If the file doesn't exist, nothing was selected from mcfly, exit without binding accept-line
if [[ ! -f $MCFLY_OUTPUT ]]; then
bind "\"$MCFLY_BASH_ACCEPT_LINE_KEYBINDING\":\"\""
return
fi
# Get the command and set the bash text to it, and move the cursor to the end of the line.
local MCFLY_COMMAND
MCFLY_COMMAND=$(command awk 'NR==2{$1=a; print substr($0, 2)}' "$MCFLY_OUTPUT")
READLINE_LINE=$MCFLY_COMMAND
READLINE_POINT=${#READLINE_LINE}
# Get the mode and bind the accept-line key if the mode is run.
local MCFLY_MODE
MCFLY_MODE=$(command awk 'NR==1{$1=a; print substr($0, 2)}' "$MCFLY_OUTPUT")
if [[ $MCFLY_MODE == "run" ]]; then
bind "\"$MCFLY_BASH_ACCEPT_LINE_KEYBINDING\":accept-line"
else
bind "\"$MCFLY_BASH_ACCEPT_LINE_KEYBINDING\":\"\""
fi
command rm -f "$MCFLY_OUTPUT"
return "$LAST_EXIT_CODE"
}
# Take ownership of ctrl-r.
if ((BASH_VERSINFO[0] >= 4)); then
# shellcheck disable=SC2016
if [[ ${MCFLY_BASH_USE_TIOCSTI-} == 1 ]]; then
bind -m emacs -x '"\C-r": "mcfly_search_with_tiocsti"'
bind -m vi-insert -x '"\C-r": "mcfly_search_with_tiocsti"'
else
# Bind ctrl+r to 2 keystrokes, the first one is used to search in McFly, the second one is used to run the command (if mcfly_search binds it to accept-line).
bind -m emacs -x "\"$MCFLY_BASH_SEARCH_KEYBINDING\":\"mcfly_search\""
bind -m vi-insert -x "\"$MCFLY_BASH_SEARCH_KEYBINDING\":\"mcfly_search\""
bind -m emacs "\"\C-r\":\"$MCFLY_BASH_SEARCH_KEYBINDING$MCFLY_BASH_ACCEPT_LINE_KEYBINDING\""
bind -m vi-insert "\"\C-r\":\"$MCFLY_BASH_SEARCH_KEYBINDING$MCFLY_BASH_ACCEPT_LINE_KEYBINDING\""
fi
else
# The logic here is:
# 1. Jump to the beginning of the edit buffer, add 'mcfly: ', and comment out the current line. We comment out the line
# to ensure that all possible special characters, including backticks, are ignored. This commented out line will
# end up as the most recent entry in the $MCFLY_HISTORY file.
# 2. Type "mcfly search" and then run the command. McFly will pull the last line from the $MCFLY_HISTORY file,
# which should be the commented-out search from step #1. It will then remove that line from the history file and
# render the search UI pre-filled with it.
bind -m emacs '"\C-r": "\C-amcfly: \e# mcfly search\C-m"'
bind -m vi-insert '"\C-r": "\e0i#mcfly: \e\C-m mcfly search\C-m"'
fi
}
mcfly_initialize
================================================
FILE: mcfly.fish
================================================
#!/usr/bin/env fish
# Avoid loading this file more than once
if test "$__MCFLY_LOADED" != "loaded"
set -g __MCFLY_LOADED "loaded"
# If this is an interactive shell
if status is-interactive
# Note: we only use the history file for the session when this file was sourced.
# Would have to reset this before calling mcfly if you want commands from another session later.
if not set -q MCFLY_HISTFILE
set -gx MCFLY_HISTFILE (set -q XDG_DATA_HOME; and echo $XDG_DATA_HOME; or echo $HOME/.local/share)/fish/(set -q fish_history; and echo $fish_history; or echo fish)_history
end
if not test -r "$MCFLY_HISTFILE"
echo "McFly: $MCFLY_HISTFILE does not exist or is not readable. Please fix this or set MCFLY_HISTFILE to something else before using McFly." >&2
exit 1
end
# MCFLY_SESSION_ID is used by McFly internally to keep track of the commands from a particular terminal session.
set -gx MCFLY_SESSION_ID (dd if=/dev/urandom bs=256 count=1 2>/dev/null | env LC_ALL=C tr -dc 'a-zA-Z0-9' | head -c 24)
# Find the binary
set -q MCFLY_PATH; or set -l MCFLY_PATH (command -v mcfly)
if test -z "$MCFLY_PATH"; or test "$MCFLY_PATH" = "mcfly not found"
echo "Cannot find the mcfly binary, please make sure that mcfly is in your path before sourcing mcfly.fish"
exit 1
end
# We don't need a MCFLY_HISTORY file because we can get the last command in fish_postexec.
set -gx MCFLY_HISTORY /dev/null
set -g __MCFLY_CMD $MCFLY_PATH --mcfly_history $MCFLY_HISTORY --history_format fish
function __mcfly_save_old_pwd -d 'Save PWD before running command' -e fish_preexec
set -g __MCFLY_OLD_PWD "$PWD"
end
function __mcfly_add_command -d 'Add run commands to McFly database' -e fish_postexec
# Retain return code of last command before we lose it
set -l last_status $status
# Check for the private mode
test -n "$fish_private_mode"; and return
# Handle first call of this function after sourcing mcfly.fish, when the old PWD won't be set
set -q __MCFLY_OLD_PWD; or set -g __MCFLY_OLD_PWD "$PWD"
test -n "$MCFLY_DEBUG"; and echo mcfly.fish: Run eval $__MCFLY_CMD add --exit '$last_status' --old-dir '$__MCFLY_OLD_PWD' -- '$argv[1]'
eval $__MCFLY_CMD add --exit '$last_status' --old-dir '$__MCFLY_OLD_PWD' -- '$argv[1]'
end
# Set up key binding functions.
function __mcfly-history-widget -d "Search command history with McFly"
set tmpdir $TMPDIR
if test -z "$tmpdir"
set tmpdir /tmp
end
set -l mcfly_output (mktemp "$tmpdir/mcfly.output.XXXXXXXX")
eval $__MCFLY_CMD search -o '$mcfly_output' -- (commandline | string escape)
# Interpret commandline/run requests from McFly
set -l mode; set -l commandline
while read key val
test "$key" = "mode"; and set mode "$val"
test "$key" = "commandline"; and set commandline "$val"
test "$key" = "delete"; and history delete --exact --case-sensitive "$val"
end < "$mcfly_output"
rm -f $mcfly_output
if test -n "$commandline"
commandline "$commandline"
end
if test "$mode" = "run"
commandline -f execute
end
commandline -f repaint
end
function mcfly_key_bindings -d "Default key bindings for McFly"
bind \cr __mcfly-history-widget
if bind -M insert >/dev/null 2>&1
bind -M insert \cr __mcfly-history-widget
end
end
mcfly_key_bindings
end
end
================================================
FILE: mcfly.ps1
================================================
#!/usr/bin/env pwsh
$null = New-Module mcfly {
# We need PSReadLine for a number of capabilities
if ($null -eq (Get-Module -Name PSReadLine)) {
Write-Host "Installing PSReadLine as McFly dependency"
Install-Module PSReadLine
}
# Get history file and make a dummy file for psreadline (hopefully after it has loaded the real history file to its in memory history)
$env:HISTFILE = $null -eq $env:HISTFILE -or "" -eq $env:HISTFILE ? (Get-PSReadLineOption).HistorySavePath : $env:HISTFILE;
$psreadline_dummy = New-TemporaryFile
# Append history to dummy file for compatibility
Get-Content -Path $Env:HISTFILE | Out-File -FilePath $psreadline_dummy -Force
Set-PSReadLineOption -HistorySavePath $psreadline_dummy.FullName
$fileExists = Test-Path -path $env:HISTFILE
if (-not $fileExists) {
Write-Host "McFly: ${env:HISTFILE} does not exist or is not readable. Please fix this or set HISTFILE to something else before using McFly.";
return 1;
}
# MCFLY_SESSION_ID is used by McFly internally to keep track of the commands from a particular terminal session.
$MCFLY_SESSION_ID = new-guid
$env:MCFLY_SESSION_ID = $MCFLY_SESSION_ID
$env:MCFLY_HISTORY = New-TemporaryFile
Get-Content $env:HISTFILE | Select-Object -Last 100 | Set-Content $env:MCFLY_HISTORY
<#
.SYNOPSIS
Cmdlet to run McFly
.PARAMETER CommandToComplete
The command to complete
.EXAMPLE
Invoke-McFly -CommandToComplete "cargo bu"
#>
function Invoke-McFly {
Param([string]$CommandToComplete)
$lastExitTmp = $LASTEXITCODE
$tempFile = New-TemporaryFile
Start-Process -FilePath '::MCFLY::' -ArgumentList "search", "$CommandToComplete", -o, "$tempFile" -NoNewWindow -Wait
foreach($line in Get-Content $tempFile) {
$key, $value = $line -split ' ', 2
if ("mode" -eq $key) {
$mode = $value
}
if ("commandline" -eq $key) {
$commandline = $value
}
}
if(-not ($null -eq $commandline)) {
[Microsoft.PowerShell.PSConsoleReadLine]::DeleteLine()
[Microsoft.PowerShell.PSConsoleReadline]::Insert($commandline)
if("run" -eq $mode) {
[Microsoft.PowerShell.PSConsoleReadline]::AcceptLine()
}
}
Remove-Item $tempFile
$LASTEXITCODE = $lastExitTmp
}
<#
.SYNOPSIS
Add a command to McFly's history.
.PARAMETER Command
The string of the command to add to McFly's history
.PARAMETER ExitCode
The exit code of the command to add
.EXAMPLE
Add-CommandToMcFly -Command "cargo build"
#>
function Add-CommandToMcFly {
Param (
[string] $Command,
[int] $ExitCode
)
$ExitCode = $ExitCode ?? 0;
$Command | Out-File -FilePath $env:MCFLY_HISTORY -Append
Start-Process -FilePath '::MCFLY::' -ArgumentList add, --exit, $ExitCode, --append-to-histfile, $env:HISTFILE -NoNewWindow | Write-Host
}
# We need to make sure we call out AddToHistoryHandler right after each command is called
Set-PSReadLineOption -HistorySaveStyle SaveIncrementally
Set-PSReadLineOption -PredictionSource HistoryAndPlugin
Set-PSReadLineOption -AddToHistoryHandler {
Param([string]$Command)
$lastExitTmp = $LASTEXITCODE
$Command = $Command.Trim();
# PSReadLine executes this before the command even runs, so we don't know its exit code - assume 0
Add-CommandToMcFly -Command $Command -ExitCode 0
$LASTEXITCODE = $lastExitTmp
# Tell PSReadLine to save the command to their in-memory history (and also the dummy file)
return $true
}
Set-PSReadLineKeyHandler -Chord "Ctrl+r" -ScriptBlock {
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadline]::GetBufferState([ref]$line, [ref]$cursor)
"#mcfly: $line" | Out-File -FilePath $env:MCFLY_HISTORY -Append
Invoke-McFly -CommandToComplete "`"$line`""
}
Export-ModuleMember -Function @(
"Invoke-McFly"
"Add-CommandToMcFly"
)
}
================================================
FILE: mcfly.zsh
================================================
#!/bin/zsh
() {
# Ensure an interactive shell
[[ -o interactive ]] || return 0
# Setup MCFLY_HISTFILE and make sure it exists.
export MCFLY_HISTFILE="${HISTFILE:-$HOME/.zsh_history}"
if [[ ! -r "${MCFLY_HISTFILE}" ]]; then
echo "McFly: ${MCFLY_HISTFILE} does not exist or is not readable. Please fix this or set HISTFILE to something else before using McFly."
return 1
fi
# MCFLY_SESSION_ID is used by McFly internally to keep track of the commands from a particular terminal session.
export MCFLY_SESSION_ID=$(command dd if=/dev/urandom bs=256 count=1 2> /dev/null | LC_ALL=C command tr -dc 'a-zA-Z0-9' | command head -c 24)
# Find the binary
MCFLY_PATH=${MCFLY_PATH:-$(command which mcfly)}
if [[ -z "$MCFLY_PATH" || "$MCFLY_PATH" == "mcfly not found" ]]; then
echo "Cannot find the mcfly binary, please make sure that mcfly is in your path before sourcing mcfly.zsh."
return 1
fi
# Required for commented out mcfly search commands to work.
setopt interactive_comments # allow comments in interactive shells (like Bash does)
# McFly's temporary, per-session history file.
export MCFLY_HISTORY=$(command mktemp ${TMPDIR:-/tmp}/mcfly.XXXXXXXX)
# Check if we need to use extended history
if [[ -o extendedhistory ]]; then
export MCFLY_HISTORY_FORMAT="zsh-extended"
else
export MCFLY_HISTORY_FORMAT="zsh"
fi
# Setup a function to be used by $PROMPT_COMMAND.
function mcfly_prompt_command {
local exit_code=$? # Record exit status of previous command.
# Populate McFly's temporary, per-session history file from recent commands in the shell's primary HISTFILE.
if [[ ! -f "${MCFLY_HISTORY}" ]]; then
export MCFLY_HISTORY=$(command mktemp ${TMPDIR:-/tmp}/mcfly.XXXXXXXX)
command tail -n100 "${MCFLY_HISTFILE}" >| ${MCFLY_HISTORY}
fi
# Write history to $MCFLY_HISTORY.
fc -W "${MCFLY_HISTORY}"
# Run mcfly with the saved code. It find the text of the last command in $MCFLY_HISTORY and save it to the database.
[ -n "$MCFLY_DEBUG" ] && echo "mcfly.zsh: Run mcfly add --exit ${exit_code}"
$MCFLY_PATH --history_format $MCFLY_HISTORY_FORMAT add --exit ${exit_code}
return ${exit_code} # Restore the original exit code by returning it.
}
if [[ -z $precmd_functions ]] || [[ "${precmd_functions[(ie)mcfly_prompt_command]}" -gt ${#precmd_functions} ]]; then
precmd_functions+=(mcfly_prompt_command)
else
[ -n "$MCFLY_DEBUG" ] && echo "mcfly_prompt_command already in precmd_functions, skipping"
fi
# Cleanup $MCFLY_HISTORY tmp files on exit.
mcfly_exit_logger() {
[ -n "$MCFLY_DEBUG" ] && echo "mcfly.zsh: Exiting and removing $MCFLY_HISTORY"
command rm -f $MCFLY_HISTORY
}
if [[ -z $zshexit_functions ]] || [[ "${zshexit_functions[(ie)mcfly_exit_logger]}" -gt ${#zshexit_functions} ]]; then
zshexit_functions+=(mcfly_exit_logger)
else
[ -n "$MCFLY_DEBUG" ] && echo "mcfly_exit_logger already in zshexit_functions, skipping"
fi
# Take ownership of ctrl-r.
mcfly-history-widget() {
() {
echoti rmkx
exec </dev/tty
local mcfly_output=$(mktemp ${TMPDIR:-/tmp}/mcfly.output.XXXXXXXX)
$MCFLY_PATH --history_format $MCFLY_HISTORY_FORMAT search -o "${mcfly_output}" "${LBUFFER}"
echoti smkx
# Interpret commandline/run requests from McFly
while read -r key val; do
if [[ "$key" = "mode" ]]; then local mode="$val"; fi
if [[ "$key" = "commandline" ]]; then local commandline="$val"; fi
done < "${mcfly_output}"
command rm -f $mcfly_output
if [[ -n $commandline ]]; then
RBUFFER=""
LBUFFER="${commandline}"
fi
if [[ "${mode}" == "run" ]]; then
zle accept-line
fi
zle redisplay
}
}
zle -N mcfly-history-widget
bindkey '^R' mcfly-history-widget
}
================================================
FILE: pkg/brew/mcfly.rb
================================================
# To install:
# brew tap cantino/mcfly
# brew install mcfly
#
# To remove:
# brew uninstall mcfly
# brew untap cantino/mcfly
class Mcfly < Formula
version 'v0.9.4'
deprecate! date: "2024-05-18", because: "is now in the core homebrew repository and you don't need this tap"
desc "McFly"
homepage "https://github.com/cantino/mcfly"
if OS.mac?
url "https://github.com/cantino/mcfly/releases/download/#{version}/mcfly-#{version}-x86_64-apple-darwin.tar.gz"
sha256 "5ab584300dc6405a4730feb08f44ac4a7cd4a3308a1f9dc28813aa2d36782c7f"
elsif OS.linux?
url "https://github.com/cantino/mcfly/releases/download/#{version}/mcfly-#{version}-x86_64-unknown-linux-musl.tar.gz"
sha256 "72d2c6fdaa111ac96c2cf725fc40e313e2856643482be58608911a09440313f1"
end
def install
bin.install "mcfly"
end
def caveats
<<~EOS
DEPRECATED! mcfly is now in the core homebrew repository and you don't need this tap.
Please run:
brew uninstall mcfly
brew untap cantino/mcfly
brew install mcfly
EOS
end
end
================================================
FILE: src/cli.rs
================================================
use clap::{Parser, Subcommand, ValueEnum};
use regex::Regex;
use std::path::PathBuf;
/// Fly through your shell history
#[derive(Parser)]
#[command(author, version)]
pub struct Cli {
#[command(subcommand)]
pub command: SubCommand,
/// Debug
#[arg(short, long)]
pub debug: bool,
/// Session ID to record or search under (defaults to $`MCFLY_SESSION_ID`)
#[arg(long = "session_id")]
pub session_id: Option<String>,
/// Shell history file to read from when adding or searching (defaults to $`MCFLY_HISTORY`)
#[arg(long = "mcfly_history")]
pub mcfly_history: Option<PathBuf>,
/// Shell history file format
#[arg(
value_name = "FORMAT",
value_enum,
long = "history_format",
default_value_t
)]
pub history_format: HistoryFormat,
}
#[derive(Subcommand)]
pub enum SubCommand {
/// Add commands to the history
#[command(alias = "a")]
Add {
/// The command that was run (default last line of $`MCFLY_HISTORY` file)
command: Vec<String>,
/// Exit code of command
#[arg(value_name = "EXIT_CODE", short, long)]
exit: Option<i32>,
/// Also append command to the given file (e.q., .`bash_history`)
#[arg(value_name = "HISTFILE", short, long)]
append_to_histfile: Option<String>,
/// The time that the command was run (default now)
#[arg(value_name = "UNIX_EPOCH", short, long)]
when: Option<i64>,
/// Directory where command was run (default $PWD)
#[arg(value_name = "PATH", short, long = "dir")]
directory: Option<String>,
/// The previous directory the user was in before running the command (default $OLDPWD)
#[arg(value_name = "PATH", short, long = "old-dir")]
old_directory: Option<String>,
},
/// Search the history
#[command(alias = "s")]
Search {
/// The command search term(s)
command: Vec<String>,
/// Directory where command was run (default $PWD)
#[arg(value_name = "PATH", short, long = "dir")]
directory: Option<String>,
/// Number of results to return
#[arg(value_name = "NUMBER", short, long)]
results: Option<u16>,
/// Fuzzy-find results. 0 is off; higher numbers weight shorter/earlier matches more. Try 2
#[arg(short, long)]
fuzzy: Option<i16>,
/// Delete entry without confirm
#[arg(long = "delete_without_confirm")]
delete_without_confirm: bool,
/// Write results to file, including selection mode, new commandline, and any shell-specific requests
#[arg(value_name = "PATH", short, long)]
output_selection: Option<String>,
},
/// Record a directory having been moved; moves command records from the old path to the new one
Move {
/// The old directory path
old_dir_path: String,
/// The new directory path
new_dir_path: String,
},
/// Train the suggestion engine (developer tool)
Train {
/// Directory where command was run
#[arg(short, long = "refresh_cache")]
refresh_cache: bool,
},
/// Prints the shell code used to execute mcfly
Init {
/// Shell to init
#[arg(value_enum)]
shell: InitMode,
},
/// Dump history into stdout; the results are sorted by timestamp
Dump {
/// Select all commands ran since the point
#[arg(long)]
since: Option<String>,
/// Select all commands ran before the point
#[arg(long)]
before: Option<String>,
/// Sort order [case ignored]
#[arg(
long,
short,
value_name = "ORDER",
value_enum,
default_value_t,
ignore_case = true
)]
sort: SortOrder,
/// Require commands to match the pattern
#[arg(long, short)]
regex: Option<Regex>,
/// The format to dump in
#[arg(long, short, value_enum, default_value_t)]
format: DumpFormat,
},
/// Prints stats
Stats {
/// The minimum command length to be listed in the "top-n" commands
#[arg(
value_name = "MIN_CMD_LENGTH",
short,
long,
value_name = "min_cmd_length",
default_value_t = 0
)]
min_cmd_length: i16,
/// The number of "top-n" commands
#[arg(
value_name = "CMDS",
short,
long,
value_name = "cmds",
default_value_t = 10
)]
cmds: i16,
/// Break down by top directories - defaults to 0, which doesn't limit by directory
#[arg(
value_name = "DIRS",
short,
long,
value_name = "dirs",
default_value_t = 0
)]
dirs: i16,
// Skip the top n commands when breaking down by directory
#[arg(
value_name = "GLOBAL_CMDS_TO_IGNORE",
short,
long,
value_name = "global_cmds_to_ignore",
default_value_t = 10
)]
global_commands_to_ignore: i16,
/// Only show commands from a given directory
#[arg(value_name = "ONLY_DIR", short, long)]
only_dir: Option<String>,
},
}
#[derive(Clone, Copy, ValueEnum, Default)]
pub enum HistoryFormat {
#[default]
Bash,
Zsh,
ZshExtended,
Fish,
}
#[derive(Clone, Copy, ValueEnum)]
pub enum InitMode {
Bash,
Zsh,
Fish,
Powershell,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
#[value(rename_all = "UPPER")]
pub enum SortOrder {
#[default]
#[value(alias = "asc")]
Asc,
#[value(alias = "desc")]
Desc,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum DumpFormat {
#[default]
Json,
Csv,
}
impl Cli {
#[must_use]
pub fn is_init(&self) -> bool {
matches!(self.command, SubCommand::Init { .. })
}
}
impl SortOrder {
#[inline]
#[must_use]
pub fn to_str(&self) -> &'static str {
match self {
Self::Asc => "ASC",
Self::Desc => "DESC",
}
}
}
================================================
FILE: src/command_input.rs
================================================
use std::fmt;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug)]
pub enum InputCommand {
Insert(char),
Backspace,
Delete,
Move(Move),
}
#[derive(Debug)]
pub enum Move {
BOL,
EOL,
BackwardWord,
ForwardWord,
Backward,
Forward,
Exact(usize),
}
#[derive(Debug)]
/// `CommandInput` data structure
pub struct CommandInput {
/// The command itself
pub command: String,
/// The current cursor position
pub cursor: usize,
/// A cache of the length of command in graphemes (not bytes or chars!)
pub len: usize,
}
impl fmt::Display for CommandInput {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.command.fmt(f)
}
}
impl CommandInput {
pub fn from<S: Into<String>>(s: S) -> CommandInput {
let mut input = CommandInput {
command: s.into(),
cursor: 0,
len: 0,
};
input.recompute_caches();
input.cursor = input.len;
input
}
pub fn clear(&mut self) {
self.command.clear();
self.recompute_caches();
}
pub fn set(&mut self, str: &str) {
self.command = str.to_string();
self.recompute_caches();
}
pub fn move_cursor(&mut self, direction: Move) {
let mut tmp: isize = self.cursor as isize;
match direction {
Move::Backward => tmp -= 1,
Move::Exact(i) => tmp = i as isize,
Move::Forward => tmp += 1,
Move::BOL => tmp = 0,
Move::EOL => tmp = self.len as isize,
Move::ForwardWord => {
tmp = self.next_word_boundary() as isize;
}
Move::BackwardWord => {
tmp = self.previous_word_boundary() as isize;
}
}
tmp = tmp.clamp(0, self.len as isize);
self.cursor = tmp as usize;
}
pub fn delete(&mut self, cmd: Move) {
let mut new_command = String::with_capacity(self.command.len());
let command_copy = self.command.clone();
let vec = command_copy.grapheme_indices(true);
match cmd {
Move::Backward => {
if self.cursor == 0 {
return;
}
self.move_cursor(Move::Backward);
for (count, (_, item)) in vec.enumerate() {
if count != self.cursor {
new_command.push_str(item);
}
}
self.command = new_command;
self.recompute_caches();
}
Move::Forward => {
if self.cursor == self.len {
return;
}
for (count, (_, item)) in vec.enumerate() {
if count != self.cursor {
new_command.push_str(item);
}
}
self.command = new_command;
self.recompute_caches();
}
Move::EOL => {
if self.cursor == self.len {
return;
}
for (count, (_, item)) in vec.enumerate() {
if count < self.cursor {
new_command.push_str(item);
}
}
self.command = new_command;
self.recompute_caches();
self.move_cursor(Move::EOL);
}
Move::BOL => {
if self.cursor == 0 {
return;
}
for (count, (_, item)) in vec.enumerate() {
if count >= self.cursor {
new_command.push_str(item);
}
}
self.command = new_command;
self.recompute_caches();
self.move_cursor(Move::BOL);
}
Move::ForwardWord => {
if self.cursor == self.len {
return;
}
let next_word_boundary = self.next_word_boundary();
for (count, (_, item)) in vec.enumerate() {
if count < self.cursor || count >= next_word_boundary {
new_command.push_str(item);
}
}
self.command = new_command;
self.recompute_caches();
}
Move::BackwardWord => {
if self.cursor == 0 {
return;
}
let previous_word_boundary = self.previous_word_boundary();
let mut removed_characters: usize = 0;
for (count, (_, item)) in vec.enumerate() {
if count < previous_word_boundary || count >= self.cursor {
new_command.push_str(item);
} else {
removed_characters += 1;
}
}
self.command = new_command;
self.recompute_caches();
let new_cursor_pos = self.cursor - removed_characters;
self.move_cursor(Move::Exact(new_cursor_pos));
}
_ => unreachable!(),
}
}
pub fn insert(&mut self, c: char) {
let mut new_command = String::with_capacity(self.command.len());
{
let vec = self.command.graphemes(true);
let mut pushed = false;
for (count, item) in vec.enumerate() {
if count == self.cursor {
pushed = true;
new_command.push(c);
}
new_command.push_str(item);
}
if !pushed {
new_command.push(c);
}
}
self.command = new_command;
self.recompute_caches();
self.move_cursor(Move::Forward);
}
fn recompute_caches(&mut self) {
self.len = self.command.graphemes(true).count();
}
/// Return the index of the grapheme cluster that represents the end of the previous word before
/// the cursor.
fn previous_word_boundary(&self) -> usize {
if self.cursor == 0 {
return 0;
}
let mut word_boundaries = self
.command
.split_word_bound_indices()
.map(|(i, _)| i)
.collect::<Vec<usize>>();
word_boundaries.push(self.command.len().to_owned());
let mut word_index: usize = 0;
let mut found_word: bool = false;
let command_copy = self.command.clone();
let vec = command_copy
.grapheme_indices(true)
.enumerate()
.collect::<Vec<(usize, (usize, &str))>>();
for &(count, (offset, _)) in vec.iter().rev() {
if count <= self.cursor {
if !found_word && (vec[count.saturating_sub(1)].1).1 == " " {
continue; // Ignore leading spaces
} else if found_word {
if offset == word_boundaries[word_index] {
// We've found the previous word boundary.
return count;
}
} else {
found_word = true;
while word_boundaries[word_index] < offset {
word_index += 1;
}
#[allow(clippy::implicit_saturating_sub)]
if word_index > 0 {
word_index -= 1;
}
}
}
}
0
}
/// Return the index of the grapheme cluster that represents the start of the next word after
/// the cursor.
fn next_word_boundary(&self) -> usize {
let command_copy = self.command.clone();
let grapheme_indices = command_copy.grapheme_indices(true);
let mut word_boundaries = self
.command
.split_word_bound_indices()
.map(|(i, _)| i)
.collect::<Vec<usize>>();
word_boundaries.push(self.command.len().to_owned());
let mut next_word_index: usize = 0;
let mut found_word: bool = false;
for (count, (offset, item)) in grapheme_indices.enumerate() {
if count >= self.cursor {
if !found_word && item == " " {
continue; // Ignore leading spaces
} else if found_word {
if offset == word_boundaries[next_word_index] {
// We've found the next word boundary.
return count;
}
} else {
found_word = true;
while word_boundaries[next_word_index] <= offset {
next_word_index += 1;
}
}
}
}
self.len
}
}
#[cfg(test)]
mod tests {
use super::CommandInput;
#[test]
fn display_works() {
let input = CommandInput::from("foo bar baz");
assert_eq!(format!("{input}"), "foo bar baz");
}
#[test]
fn next_word_boundary_works() {
let mut input = CommandInput::from("foo bar baz");
input.cursor = 0;
assert_eq!(input.next_word_boundary(), 3);
input.cursor = 3;
assert_eq!(input.next_word_boundary(), 7);
input.cursor = 4;
assert_eq!(input.next_word_boundary(), 7);
input.cursor = 5;
assert_eq!(input.next_word_boundary(), 7);
input.cursor = 6;
assert_eq!(input.next_word_boundary(), 7);
input.cursor = 7;
assert_eq!(input.next_word_boundary(), 11);
input.cursor = 11;
assert_eq!(input.next_word_boundary(), 11);
input.cursor = 12;
assert_eq!(input.next_word_boundary(), 11);
}
#[test]
fn previous_word_boundary_works() {
let mut input = CommandInput::from("foo bar baz");
input.cursor = 0;
assert_eq!(input.previous_word_boundary(), 0);
input.cursor = 1;
assert_eq!(input.previous_word_boundary(), 0);
input.cursor = 3;
assert_eq!(input.previous_word_boundary(), 0);
input.cursor = 4;
assert_eq!(input.previous_word_boundary(), 0);
input.cursor = 5;
assert_eq!(input.previous_word_boundary(), 4);
input.cursor = 7;
assert_eq!(input.previous_word_boundary(), 4);
input.cursor = 8;
assert_eq!(input.previous_word_boundary(), 4);
input.cursor = 11;
assert_eq!(input.previous_word_boundary(), 8);
input.cursor = 12;
assert_eq!(input.previous_word_boundary(), 8);
}
}
================================================
FILE: src/dumper.rs
================================================
use std::io::{self, BufWriter, Write};
use crate::cli::DumpFormat;
use crate::history::{DumpCommand, History};
use crate::settings::Settings;
use crate::time::to_datetime;
#[derive(Debug)]
pub struct Dumper<'a> {
settings: &'a Settings,
history: &'a History,
}
impl<'a> Dumper<'a> {
#[inline]
pub fn new(settings: &'a Settings, history: &'a History) -> Self {
Self { settings, history }
}
pub fn dump(&self) {
let mut commands = self
.history
.dump(&self.settings.time_range, &self.settings.sort_order);
if commands.is_empty() {
println!("McFly: No history");
return;
}
if let Some(pat) = &self.settings.pattern {
commands.retain(|dc| pat.is_match(&dc.cmd));
}
match self.settings.dump_format {
DumpFormat::Json => Self::dump2json(&commands),
DumpFormat::Csv => Self::dump2csv(&commands),
}
.unwrap_or_else(|err| panic!("McFly error: Failed while output history ({err})"));
}
}
impl Dumper<'_> {
fn dump2json(commands: &[DumpCommand]) -> io::Result<()> {
let mut stdout = BufWriter::new(io::stdout().lock());
serde_json::to_writer_pretty(&mut stdout, commands).map_err(io::Error::from)?;
stdout.flush()
}
fn dump2csv(commands: &[DumpCommand]) -> io::Result<()> {
let mut wtr = csv::Writer::from_writer(io::stdout().lock());
wtr.write_record(["cmd", "when_run"])?;
for dc in commands {
wtr.write_record([dc.cmd.as_str(), to_datetime(dc.when_run).as_str()])?;
}
wtr.flush()
}
}
================================================
FILE: src/fake_typer.rs
================================================
#[cfg(not(windows))]
use libc;
// Should we be using https://docs.rs/libc/0.2.44/libc/fn.ioctl.html instead?
#[cfg(not(windows))]
unsafe extern "C" {
pub fn ioctl(fd: libc::c_int, request: libc::c_ulong, arg: ...) -> libc::c_int;
}
#[cfg(not(windows))]
#[allow(clippy::useless_conversion)]
pub fn use_tiocsti(string: &str) {
for byte in string.as_bytes() {
let a: *const u8 = byte;
assert!(
unsafe { ioctl(0, libc::TIOCSTI.try_into().unwrap(), a) } >= 0,
"Error encountered when calling ioctl"
);
}
}
#[cfg(windows)]
pub fn use_tiocsti(string: &str) {
autopilot::key::type_string(string, &[], 0.0, 0.0);
}
================================================
FILE: src/fixed_length_grapheme_string.rs
================================================
use std::io::Write;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug)]
pub struct FixedLengthGraphemeString {
pub string: String,
pub grapheme_length: u16,
pub max_grapheme_length: u16,
}
impl Write for FixedLengthGraphemeString {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let s = String::from_utf8(buf.to_vec()).unwrap();
self.push_str(&s);
Ok(s.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl FixedLengthGraphemeString {
#[must_use]
pub fn empty(max_grapheme_length: u16) -> FixedLengthGraphemeString {
FixedLengthGraphemeString {
string: String::new(),
grapheme_length: 0,
max_grapheme_length,
}
}
pub fn new<S: Into<String>>(s: S, max_grapheme_length: u16) -> FixedLengthGraphemeString {
let mut fixed_length_grapheme_string =
FixedLengthGraphemeString::empty(max_grapheme_length);
fixed_length_grapheme_string.push_grapheme_str(s);
fixed_length_grapheme_string
}
pub fn push_grapheme_str<S: Into<String>>(&mut self, s: S) {
for grapheme in s.into().graphemes(true) {
if self.grapheme_length >= self.max_grapheme_length {
return;
}
self.string.push_str(grapheme);
self.grapheme_length += 1;
}
}
pub fn push_str(&mut self, s: &str) {
self.string.push_str(s);
}
}
#[cfg(test)]
mod tests {
use super::FixedLengthGraphemeString;
#[test]
fn length_works() {
let input = FixedLengthGraphemeString::new("こんにちは世界", 20);
assert_eq!(input.grapheme_length, 7);
}
#[test]
fn max_length_works() {
let mut input = FixedLengthGraphemeString::new("こんにちは世界", 5);
assert_eq!(input.string, "こんにちは");
input.push_grapheme_str("世界");
assert_eq!(input.string, "こんにちは");
input.max_grapheme_length = 7;
input.push_grapheme_str("世界");
assert_eq!(input.string, "こんにちは世界");
}
}
================================================
FILE: src/history/db_extensions.rs
================================================
use crate::history::history::Features;
use crate::network::Network;
use rusqlite::Connection;
use rusqlite::functions::FunctionFlags;
pub fn add_db_functions(db: &Connection) {
let network = Network::default();
db.create_scalar_function(
"nn_rank",
10,
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
move |ctx| {
let age_factor = ctx.get::<f64>(0)?;
let length_factor = ctx.get::<f64>(1)?;
let exit_factor = ctx.get::<f64>(2)?;
let recent_failure_factor = ctx.get::<f64>(3)?;
let selected_dir_factor = ctx.get::<f64>(4)?;
let dir_factor = ctx.get::<f64>(5)?;
let overlap_factor = ctx.get::<f64>(6)?;
let immediate_overlap_factor = ctx.get::<f64>(7)?;
let selected_occurrences_factor = ctx.get::<f64>(8)?;
let occurrences_factor = ctx.get::<f64>(9)?;
let features = Features {
age_factor,
length_factor,
exit_factor,
recent_failure_factor,
selected_dir_factor,
dir_factor,
overlap_factor,
immediate_overlap_factor,
selected_occurrences_factor,
occurrences_factor,
};
Ok(network.output(&features))
},
)
.unwrap_or_else(|err| panic!("McFly error: Successful create_scalar_function ({err})"));
}
================================================
FILE: src/history/history.rs
================================================
#![allow(clippy::module_inception)]
use crate::cli::SortOrder;
use crate::history::{db_extensions, schema};
use crate::network::Network;
use crate::path_update_helpers;
use crate::settings::{HistoryFormat, ResultFilter, ResultSort, Settings, TimeRange};
use crate::shell_history;
use crate::simplified_command::SimplifiedCommand;
use crate::time::to_datetime;
use itertools::Itertools;
use rusqlite::named_params;
use rusqlite::types::ToSql;
use rusqlite::{Connection, MappedRows, Row};
use serde::{Serialize, Serializer};
use std::cmp::Ordering;
use std::io::Write;
use std::path::PathBuf;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use std::{fmt, fs, io};
#[derive(Debug, Clone, Default)]
pub struct Features {
pub age_factor: f64,
pub length_factor: f64,
pub exit_factor: f64,
pub recent_failure_factor: f64,
pub selected_dir_factor: f64,
pub dir_factor: f64,
pub overlap_factor: f64,
pub immediate_overlap_factor: f64,
pub selected_occurrences_factor: f64,
pub occurrences_factor: f64,
}
#[derive(Debug, Clone, Default)]
pub struct Command {
pub id: i64,
pub cmd: String,
pub cmd_tpl: String,
pub session_id: String,
pub rank: f64,
pub when_run: Option<i64>,
pub last_run: Option<i64>,
pub exit_code: Option<i32>,
pub selected: bool,
pub dir: Option<String>,
pub features: Features,
pub match_indices: Vec<usize>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DumpCommand {
pub cmd: String,
#[serde(serialize_with = "ser_to_datetime")]
pub when_run: i64,
}
impl fmt::Display for Command {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.cmd.fmt(f)
}
}
impl From<Command> for String {
fn from(command: Command) -> Self {
command.cmd
}
}
#[inline]
fn ser_to_datetime<S>(when_run: &i64, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&to_datetime(*when_run))
}
#[derive(Debug)]
pub struct History {
pub connection: Connection,
pub network: Network,
}
const IGNORED_COMMANDS: [&str; 7] = [
"pwd",
"ls",
"cd",
"cd ..",
"clear",
"history",
"mcfly search",
];
impl History {
#[must_use]
pub fn load(history_format: HistoryFormat) -> History {
let db_path = Settings::mcfly_db_path();
let history = if db_path.exists() {
History::from_db_path(db_path)
} else {
History::from_shell_history(history_format)
};
schema::migrate(&history.connection);
history
}
pub fn should_add(&self, command: &str) -> bool {
// Ignore empty commands.
if command.is_empty() {
return false;
}
// Ignore commands added via a ctrl-r search.
if command.starts_with("#mcfly:") {
return false;
}
// Ignore commands with a leading space.
if command.starts_with(' ') {
return false;
}
// Ignore blacklisted commands.
if IGNORED_COMMANDS.contains(&command) {
return false;
}
// Ignore the previous command (independent of Session ID) so that opening a new terminal
// window won't replay the last command in the history.
let last_command = self.last_command(&None);
if last_command.is_none() {
return true;
}
!command.eq(&last_command.unwrap().cmd)
}
pub fn add(
&self,
command: &str,
session_id: &str,
dir: &str,
when_run: &Option<i64>,
exit_code: Option<i32>,
old_dir: &Option<String>,
) {
self.possibly_update_paths(command, exit_code);
let selected = self.determine_if_selected_from_ui(command, session_id, dir);
let simplified_command = SimplifiedCommand::new(command, true);
self.connection.execute("INSERT INTO commands (cmd, cmd_tpl, session_id, when_run, exit_code, selected, dir, old_dir) VALUES (:cmd, :cmd_tpl, :session_id, :when_run, :exit_code, :selected, :dir, :old_dir)",
named_params!{
":cmd": &command.to_owned(),
":cmd_tpl": &simplified_command.result,
":session_id": &session_id.to_owned(),
":when_run": &when_run.to_owned(),
":exit_code": &exit_code.clone(),
":selected": &selected,
":dir": &dir.to_owned(),
":old_dir": &old_dir.to_owned(),
}).unwrap_or_else(|err| panic!("McFly error: Insert into commands to work ({err})"));
}
fn determine_if_selected_from_ui(&self, command: &str, session_id: &str, dir: &str) -> bool {
let rows_affected = self
.connection
.execute(
"DELETE FROM selected_commands \
WHERE cmd = :cmd \
AND session_id = :session_id \
AND dir = :dir",
&[
(":cmd", &command.to_owned()),
(":session_id", &session_id.to_owned()),
(":dir", &dir.to_owned()),
],
)
.unwrap_or_else(|err| {
panic!("McFly error: DELETE from selected_commands to work ({err})")
});
// Delete any other pending selected commands for this session -- they must have been aborted or edited.
self.connection
.execute(
"DELETE FROM selected_commands WHERE session_id = :session_id",
&[(":session_id", &session_id.to_owned())],
)
.unwrap_or_else(|err| {
panic!("McFly error: DELETE from selected_commands to work ({err})")
});
rows_affected > 0
}
pub fn record_selected_from_ui(&self, command: &str, session_id: &str, dir: &str) {
self.connection.execute("INSERT INTO selected_commands (cmd, session_id, dir) VALUES (:cmd, :session_id, :dir)",
&[
(":cmd", &command.to_owned()),
(":session_id", &session_id.to_owned()),
(":dir", &dir.to_owned())
]).unwrap_or_else(|err| panic!("McFly error: Insert into selected_commands to work ({err})"));
}
// Update historical paths in our database if a directory has been renamed or moved.
pub fn possibly_update_paths(&self, command: &str, exit_code: Option<i32>) {
let successful = exit_code.is_none() || exit_code.unwrap() == 0;
let is_move =
|c: &str| c.to_lowercase().starts_with("mv ") && !c.contains('*') && !c.contains('?');
if successful && is_move(command) {
let parts = path_update_helpers::parse_mv_command(command);
if parts.len() == 2 {
let normalized_from = path_update_helpers::normalize_path(&parts[0]);
let normalized_to = path_update_helpers::normalize_path(&parts[1]);
// If $to/$(base_name($from)) exists, and is a directory, assume we've moved $from into $to.
// If not, assume we've renamed $from to $to.
if let Some(basename) = PathBuf::from(&normalized_from).file_name() {
if let Some(utf8_basename) = basename.to_str() {
if utf8_basename.contains('.') {
// It was probably a file.
return;
}
let maybe_moved_directory =
PathBuf::from(&normalized_to).join(utf8_basename);
if maybe_moved_directory.exists() {
if maybe_moved_directory.is_dir() {
self.update_paths(
&normalized_from,
maybe_moved_directory.to_str().unwrap(),
false,
);
} else {
// The source must have been a file, so ignore it.
}
return;
}
} else {
// Don't try to handle non-utf8 filenames, at least for now.
return;
}
}
let to_pathbuf = PathBuf::from(&normalized_to);
if to_pathbuf.exists() && to_pathbuf.is_dir() {
self.update_paths(&normalized_from, &normalized_to, false);
}
}
}
}
pub fn find_matches(
&self,
cmd: &str,
num: i16,
fuzzy: i16,
result_sort: &ResultSort,
) -> Vec<Command> {
let (wildcard, match_function, cmd) = if Self::is_case_sensitive(cmd) {
// escape '*' with '[*]' and replace '%' with '*' for glob matching
("*", "GLOB", cmd.replace("*", "[*]").replace("%", "*"))
} else {
("%", "LIKE", cmd.to_string())
};
let mut like_query = wildcard.to_string();
if fuzzy > 0 {
like_query.push_str(&cmd.split("").collect::<Vec<&str>>().join(wildcard));
} else {
like_query.push_str(&cmd);
}
like_query += wildcard;
let order_by_column: &str = match &result_sort {
ResultSort::LastRun => "last_run",
_ => "rank",
};
let query: &str = &format!(
"{} {} {} {} {}",
"SELECT id, cmd, cmd_tpl, session_id, when_run, exit_code, selected, dir, rank,
age_factor, length_factor, exit_factor, recent_failure_factor,
selected_dir_factor, dir_factor, overlap_factor, immediate_overlap_factor,
selected_occurrences_factor, occurrences_factor, last_run
FROM contextual_commands
WHERE cmd",
match_function,
"(:like)
ORDER BY",
order_by_column,
"DESC LIMIT :limit"
)[..];
let mut statement = self
.connection
.prepare(query)
.unwrap_or_else(|err| panic!("McFly error: Prepare to work ({err})"));
let command_iter = statement
.query_map(
named_params! { ":like": &like_query, ":limit": &num },
|row| {
let text: String = row
.get(1)
.unwrap_or_else(|err| panic!("McFly error: cmd to be readable ({err})"));
let bounds = Self::calc_match_indices(&text, &cmd, fuzzy);
Ok(Command {
id: row.get(0).unwrap_or_else(|err| {
panic!("McFly error: id to be readable ({err})")
}),
cmd: text,
cmd_tpl: row.get(2).unwrap_or_else(|err| {
panic!("McFly error: cmd_tpl to be readable ({err})")
}),
session_id: row.get(3).unwrap_or_else(|err| {
panic!("McFly error: session_id to be readable ({err})")
}),
when_run: row.get(4).unwrap_or_else(|err| {
panic!("McFly error: when_run to be readable ({err})")
}),
exit_code: row.get(5).unwrap_or_else(|err| {
panic!("McFly error: exit_code to be readable ({err})")
}),
selected: row.get(6).unwrap_or_else(|err| {
panic!("McFly error: selected to be readable ({err})")
}),
dir: row.get(7).unwrap_or_else(|err| {
panic!("McFly error: dir to be readable ({err})")
}),
rank: row.get(8).unwrap_or_else(|err| {
panic!("McFly error: rank to be readable ({err})")
}),
match_indices: bounds,
features: Features {
age_factor: row.get(9).unwrap_or_else(|err| {
panic!("McFly error: age_factor to be readable ({err})")
}),
length_factor: row.get(10).unwrap_or_else(|err| {
panic!("McFly error: length_factor to be readable ({err})")
}),
exit_factor: row.get(11).unwrap_or_else(|err| {
panic!("McFly error: exit_factor to be readable ({err})")
}),
recent_failure_factor: row.get(12).unwrap_or_else(|err| {
panic!(
"McFly error: recent_failure_factor to be readable ({err})"
)
}),
selected_dir_factor: row.get(13).unwrap_or_else(|err| {
panic!("McFly error: selected_dir_factor to be readable ({err})")
}),
dir_factor: row.get(14).unwrap_or_else(|err| {
panic!("McFly error: dir_factor to be readable ({err})")
}),
overlap_factor: row.get(15).unwrap_or_else(|err| {
panic!("McFly error: overlap_factor to be readable ({err})")
}),
immediate_overlap_factor: row.get(16).unwrap_or_else(|err| {
panic!(
"McFly error: immediate_overlap_factor to be readable ({err})"
)
}),
selected_occurrences_factor: row.get(17).unwrap_or_else(|err| {
panic!(
"McFly error: selected_occurrences_factor to be readable ({err})"
)
}),
occurrences_factor: row.get(18).unwrap_or_else(|err| {
panic!("McFly error: occurrences_factor to be readable ({err})")
}),
},
last_run: row.get(19).unwrap_or_else(|err| {
panic!("McFly error: last_run to be readable ({err})")
}),
})
},
)
.unwrap_or_else(|err| panic!("McFly error: Query Map to work ({err})"));
let mut names = Vec::new();
for result in command_iter {
names.push(result.unwrap_or_else(|err| {
panic!("McFly error: Unable to load command from DB ({err})")
}));
}
if fuzzy > 0 && result_sort != &ResultSort::LastRun {
names = names
.into_iter()
.sorted_unstable_by(|a, b| {
// Fuzzy matches impose new ordering criteria on top of the
// natural rank-based sorting: at the most basic level,
// shorter and earlier matches are more likely to be
// desired than longer or later matches -- even if they are
// ranked a little lower.
//
// Each match is weighted by the inverse of its length plus
// start position, relative to the total length + start of
// both matches added together. This yields two complements
// which always add up to 1 (e.g. 0.6 vs 0.4). If both
// matches have the same length and start position, or if
// those balance out exactly, the resulting weights will
// both equal 0.5.
//
// The weights are multiplied by the configurable fuzzy
// factor before being added to each result's original
// rank. Factors > 1 are a "thumb on the scale" increasing
// the likelihood of the weight flipping the outcome for
// the originally lower-ranked result.
let a_start = *a.match_indices.first().unwrap_or(&0);
let b_start = *b.match_indices.first().unwrap_or(&0);
let a_len = a.match_indices.last().map(|i| i + 1).unwrap_or(0) - a_start;
let b_len = b.match_indices.last().map(|i| i + 1).unwrap_or(0) - b_start;
let a_mod =
1.0 - (a_start + a_len) as f64 / (a_start + b_start + a_len + b_len) as f64;
let b_mod =
1.0 - (b_start + b_len) as f64 / (a_start + b_start + a_len + b_len) as f64;
PartialOrd::partial_cmp(
&(b.rank + b_mod * f64::from(fuzzy)),
&(a.rank + a_mod * f64::from(fuzzy)),
)
.unwrap_or(Ordering::Equal)
})
.collect();
}
names
}
/// Enable case sensitivity when input string contains uppercase
fn is_case_sensitive(cmd: &str) -> bool {
cmd.chars().any(|c| c.is_uppercase())
}
/// Calculate the indices of the matches in the text.
fn calc_match_indices(text: &str, cmd: &str, fuzzy: i16) -> Vec<usize> {
let (text, cmd) = if Self::is_case_sensitive(cmd) {
(text.to_string(), cmd.to_string())
} else {
(text.to_lowercase(), cmd.to_lowercase())
};
match fuzzy {
0 => text
.match_indices(&cmd)
.flat_map(|(index, _)| index..index + cmd.len())
.collect(),
_ => {
let mut search_iter = cmd.chars().peekable();
text.match_indices(|c| {
let next = search_iter.peek();
if next.is_some() && next.unwrap() == &c {
let _advance = search_iter.next();
return true;
}
false
})
.map(|m| m.0)
.collect()
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_cache_table(
&self,
dir: &str,
result_filter: &ResultFilter,
session_id: &Option<String>,
start_time: Option<i64>,
end_time: Option<i64>,
now: Option<i64>,
limit: Option<i64>,
) {
let lookback: u16 = 3;
let mut last_commands = self.last_command_templates(session_id, lookback as i16, 0);
if last_commands.len() < lookback as usize {
last_commands = self.last_command_templates(&None, lookback as i16, 0);
while last_commands.len() < lookback as usize {
last_commands.push(String::new());
}
}
#[allow(unused_variables)]
let beginning_of_execution = Instant::now();
self.connection
.execute("PRAGMA temp_store = MEMORY;", [])
.unwrap();
self.connection
.execute("DROP TABLE IF EXISTS temp.contextual_commands;", [])
.unwrap_or_else(|err| panic!("McFly error: Removal of temp table to work ({err})"));
let (mut when_run_min, when_run_max): (f64, f64) = self
.connection
.query_row(
"SELECT IFNULL(MIN(when_run), 0), IFNULL(MAX(when_run), 0) FROM commands",
[],
|row| Ok((row.get_unwrap(0), row.get_unwrap(1))),
)
.unwrap_or_else(|err| panic!("McFly error: Query to work ({err})"));
if (when_run_min - when_run_max).abs() < f64::EPSILON {
when_run_min -= 60.0 * 60.0;
}
let max_occurrences: f64 = self
.connection
.query_row(
"SELECT COUNT(*) AS c FROM commands GROUP BY cmd ORDER BY c DESC LIMIT 1",
[],
|row| row.get(0),
)
.unwrap_or(1.0);
let max_selected_occurrences: f64 = self.connection
.query_row("SELECT COUNT(*) AS c FROM commands WHERE selected = 1 GROUP BY cmd ORDER BY c DESC LIMIT 1", [],
|row| row.get(0)).unwrap_or(1.0); // FIXME: 1.0 seems wrong.
let max_length: f64 = self
.connection
.query_row(
"SELECT IFNULL(MAX(LENGTH(cmd)), 0) FROM commands",
[],
|row| row.get(0),
)
.unwrap_or(100.0);
let max_id: i64 = self
.connection
.query_row("SELECT IFNULL(MAX(id), 0) FROM commands", [], |row| {
row.get(0)
})
.unwrap_or(0);
let min_id = if let Some(limit_value) = limit {
if limit_value > max_id {
0
} else {
(max_id as f64 * (1.0 - (limit_value as f64 / max_id as f64))) as i64
}
} else {
0
};
let dir_filter_off = match &result_filter {
ResultFilter::Global => true,
ResultFilter::CurrentDirectory => false,
};
self.connection.execute(
"CREATE TEMP TABLE contextual_commands AS SELECT
id, cmd, cmd_tpl, session_id, when_run, MAX(when_run) AS last_run, exit_code, selected, dir,
/* to be filled in later */
0.0 AS rank,
/* length of the command string */
LENGTH(c.cmd) / :max_length AS length_factor,
/* age of the last execution of this command (0.0 is new, 1.0 is old) */
MIN((:when_run_max - when_run) / :history_duration) AS age_factor,
/* average error state (1: always successful, 0: always errors) */
SUM(CASE WHEN exit_code = 0 THEN 1.0 ELSE 0.0 END) / COUNT(*) as exit_factor,
/* recent failure (1 if failed recently, 0 if not) */
MAX(CASE WHEN exit_code != 0 AND :now - when_run < 120 THEN 1.0 ELSE 0.0 END) AS recent_failure_factor,
/* percentage run in this directory (1: always run in this directory, 0: never run in this directory) */
SUM(CASE WHEN dir = :directory THEN 1.0 ELSE 0.0 END) / COUNT(*) as dir_factor,
/* percentage of time selected in this directory (1: only selected in this dir, 0: only selected elsewhere) */
SUM(CASE WHEN dir = :directory AND selected = 1 THEN 1.0 ELSE 0.0 END) / (SUM(CASE WHEN selected = 1 THEN 1.0 ELSE 0.0 END) + 1) as selected_dir_factor,
/* average contextual overlap of this command (0: none of the last 3 commands has ever overlapped with this command, 1: all of the last three commands always overlap with this command) */
SUM((
SELECT COUNT(DISTINCT c2.cmd_tpl) FROM commands c2
WHERE c2.id >= c.id - :lookback AND c2.id < c.id AND c2.cmd_tpl IN (:last_commands0, :last_commands1, :last_commands2)
) / :lookback_f64) / COUNT(*) AS overlap_factor,
/* average overlap with the last command (0: this command never follows the last command, 1: this command always follows the last command) */
SUM((SELECT COUNT(*) FROM commands c2 WHERE c2.id = c.id - 1 AND c2.cmd_tpl = :last_commands0)) / COUNT(*) AS immediate_overlap_factor,
/* percentage selected (1: this is the most commonly selected command, 0: this command is never selected) */
SUM(CASE WHEN selected = 1 THEN 1.0 ELSE 0.0 END) / :max_selected_occurrences AS selected_occurrences_factor,
/* percentage of time this command is run relative to the most common command (1: this is the most common command, 0: this is the least common command) */
COUNT(*) / :max_occurrences AS occurrences_factor
FROM commands c
WHERE id > :min_id AND when_run > :start_time AND when_run < :end_time AND (:dir_filter_off OR dir LIKE :directory)
GROUP BY cmd
ORDER BY id DESC;",
named_params! {
":when_run_max": &when_run_max,
":history_duration": &(when_run_max - when_run_min),
":directory": &dir.to_owned(),
":dir_filter_off": &dir_filter_off,
":max_occurrences": &max_occurrences,
":max_length": &max_length,
":max_selected_occurrences": &max_selected_occurrences,
":lookback": &lookback,
":lookback_f64": &f64::from(lookback),
":last_commands0": &last_commands[0].clone(),
":last_commands1": &last_commands[1].clone(),
":last_commands2": &last_commands[2].clone(),
":start_time": &start_time.unwrap_or(0).to_owned(),
":end_time": &end_time.unwrap_or(SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_else(|err| panic!("McFly error: Time went backwards ({err})")).as_secs() as i64).to_owned(),
":now": &now.unwrap_or(SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_else(|err| panic!("McFly error: Time went backwards ({err})")).as_secs() as i64).to_owned(),
":min_id": &min_id,
}).unwrap_or_else(|err| panic!("McFly error: Creation of temp table to work ({err})"));
self.connection
.execute(
"UPDATE contextual_commands
SET rank = nn_rank(age_factor, length_factor, exit_factor,
recent_failure_factor, selected_dir_factor, dir_factor,
overlap_factor, immediate_overlap_factor,
selected_occurrences_factor, occurrences_factor);",
[],
)
.unwrap_or_else(|err| panic!("McFly error: Ranking of temp table to work ({err})"));
self.connection
.execute("CREATE INDEX temp.MyIndex ON contextual_commands(id);", [])
.unwrap_or_else(|err| {
panic!("McFly error: Creation of index on temp table to work ({err})")
});
// println!("Seconds: {}", (beginning_of_execution.elapsed().as_secs() as f64) + (beginning_of_execution.elapsed().subsec_nanos() as f64 / 1000_000_000.0));
}
pub fn commands(
&self,
session_id: &Option<String>,
num: i16,
offset: u16,
random: bool,
) -> Vec<Command> {
let order = if random { "RANDOM()" } else { "id" };
let query = if session_id.is_none() {
format!(
"SELECT id, cmd, cmd_tpl, session_id, when_run, exit_code, selected, dir FROM commands ORDER BY {order} DESC LIMIT :limit OFFSET :offset"
)
} else {
format!(
"SELECT id, cmd, cmd_tpl, session_id, when_run, exit_code, selected, dir FROM commands WHERE session_id = :session_id ORDER BY {order} DESC LIMIT :limit OFFSET :offset"
)
};
let closure: fn(&Row) -> rusqlite::Result<Command> = |row| {
Ok(Command {
id: row.get(0)?,
cmd: row.get(1)?,
cmd_tpl: row.get(2)?,
session_id: row.get(3)?,
when_run: row.get(4)?,
exit_code: row.get(5)?,
selected: row.get(6)?,
dir: row.get(7)?,
..Command::default()
})
};
if session_id.is_none() {
self.run_query(&query, &[(":limit", &num), (":offset", &offset)], closure)
} else {
self.run_query(
&query,
&[
(":session_id", &session_id.to_owned().unwrap()),
(":limit", &num),
(":offset", &offset),
],
closure,
)
}
}
pub fn run_query<T, F>(&self, query: &str, params: &[(&str, &dyn ToSql)], f: F) -> Vec<T>
where
F: FnMut(&Row<'_>) -> rusqlite::Result<T>,
{
let mut statement = self.connection.prepare(query).unwrap();
let rows: MappedRows<_> = statement
.query_map(params, f)
.unwrap_or_else(|err| panic!("McFly error: Query Map to work ({err})"));
let mut vec: Vec<T> = Vec::new();
for row in rows.flatten() {
vec.push(row);
}
vec
}
pub fn last_command(&self, session_id: &Option<String>) -> Option<Command> {
self.commands(session_id, 1, 0, false).first().cloned()
}
pub fn last_command_templates(
&self,
session_id: &Option<String>,
num: i16,
offset: u16,
) -> Vec<String> {
self.commands(session_id, num, offset, false)
.iter()
.map(|command| command.cmd_tpl.clone())
.collect()
}
pub fn delete_command(&self, command: &str) {
self.connection
.execute(
"DELETE FROM selected_commands WHERE cmd = :command",
&[(":command", &command)],
)
.unwrap_or_else(|err| {
panic!("McFly error: DELETE from selected_commands to work ({err})")
});
self.connection
.execute(
"DELETE FROM commands WHERE cmd = :command",
&[(":command", &command)],
)
.unwrap_or_else(|err| panic!("McFly error: DELETE from commands to work ({err})"));
}
pub fn update_paths(&self, old_path: &str, new_path: &str, print_output: bool) {
let normalized_old_path = path_update_helpers::normalize_path(old_path);
let normalized_new_path = path_update_helpers::normalize_path(new_path);
if normalized_old_path.len() > 1 && normalized_new_path.len() > 1 {
let like_query = normalized_old_path.to_string() + "/%";
let mut dir_update_statement = self.connection.prepare(
"UPDATE commands SET dir = :new_dir || SUBSTR(dir, :length) WHERE dir = :exact OR dir LIKE (:like)"
).unwrap();
let mut old_dir_update_statement = self.connection.prepare(
"UPDATE commands SET old_dir = :new_dir || SUBSTR(old_dir, :length) WHERE old_dir = :exact OR old_dir LIKE (:like)"
).unwrap();
let affected = dir_update_statement
.execute(named_params! {
":like": &like_query,
":exact": &normalized_old_path,
":new_dir": &normalized_new_path,
":length": &(normalized_old_path.chars().count() as u32 + 1),
})
.unwrap_or_else(|err| panic!("McFly error: dir UPDATE to work ({err})"));
old_dir_update_statement
.execute(named_params! {
":like": &like_query,
":exact": &normalized_old_path,
":new_dir": &normalized_new_path,
":length": &(normalized_old_path.chars().count() as u32 + 1),
})
.unwrap_or_else(|err| panic!("McFly error: old_dir UPDATE to work ({err})"));
if print_output {
println!(
"McFly: Command database paths renamed from {normalized_old_path} to {normalized_new_path} (affected {affected} commands)"
);
}
} else if print_output {
println!("McFly: Not updating paths due to invalid options.");
}
}
pub fn dump(&self, time_range: &TimeRange, order: &SortOrder) -> Vec<DumpCommand> {
let mut where_clause = String::new();
// Were there condtions in where clause?
let mut has_conds = false;
let mut params: Vec<(&str, &dyn ToSql)> = Vec::with_capacity(2);
if !time_range.is_full() {
where_clause.push_str("WHERE");
if let Some(since) = &time_range.since {
where_clause.push_str(" :since <= when_run");
has_conds = true;
params.push((":since", since));
}
if let Some(before) = &time_range.before {
if has_conds {
where_clause.push_str(" AND");
}
where_clause.push_str(" when_run < :before");
params.push((":before", before));
}
}
let query = format!(
"SELECT cmd, when_run FROM commands {} ORDER BY when_run {}",
where_clause,
order.to_str()
);
self.run_query(&query, params.as_slice(), |row| {
Ok(DumpCommand {
cmd: row.get(0)?,
when_run: row.get(1)?,
})
})
}
fn from_shell_history(history_format: HistoryFormat) -> History {
print!(
"McFly: Importing shell history for the first time. This may take a minute or two..."
);
io::stdout()
.flush()
.unwrap_or_else(|err| panic!("McFly error: STDOUT flush should work ({err})"));
// Load this first to make sure it works before we create the DB.
let commands =
shell_history::full_history(&shell_history::history_file_path(), history_format);
// Use ~/.mcfly if it already exists, or create 'mcfly' folder in XDG_DATA_DIR
let mcfly_db_path = Settings::mcfly_db_path();
let mcfly_db_dir = mcfly_db_path.parent().unwrap();
fs::create_dir_all(mcfly_db_dir)
.unwrap_or_else(|_| panic!("Unable to create {mcfly_db_dir:?}"));
// Make ~/.mcfly/history.db
let mut connection = Connection::open(&mcfly_db_path)
.unwrap_or_else(|_| panic!("Unable to create history DB at {:?}", &mcfly_db_path));
db_extensions::add_db_functions(&connection);
connection.execute_batch(
"CREATE TABLE commands( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
cmd TEXT NOT NULL, \
cmd_tpl TEXT, \
session_id TEXT NOT NULL, \
when_run INTEGER NOT NULL, \
exit_code INTEGER NOT NULL, \
selected INTEGER NOT NULL, \
dir TEXT, \
old_dir TEXT \
); \
CREATE INDEX command_cmds ON commands (cmd);\
CREATE INDEX command_session_id ON commands (session_id);\
CREATE INDEX command_dirs ON commands (dir);\
\
CREATE TABLE selected_commands( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
cmd TEXT NOT NULL, \
session_id TEXT NOT NULL, \
dir TEXT NOT NULL \
); \
CREATE INDEX selected_command_session_cmds ON selected_commands (session_id, cmd);"
).unwrap_or_else(|err| panic!("McFly error: Unable to initialize history db ({err})"));
let transaction = connection
.transaction()
.unwrap_or_else(|err| panic!("McFly error: Unable to begin transaction ({err})"));
{
let mut statement = transaction
.prepare("INSERT INTO commands (cmd, cmd_tpl, session_id, when_run, exit_code, selected) VALUES (:cmd, :cmd_tpl, :session_id, :when_run, :exit_code, :selected)")
.unwrap_or_else(|err| panic!("McFly error: Unable to prepare insert ({err})"));
for command in commands {
if !IGNORED_COMMANDS.contains(&command.command.as_str()) {
let simplified_command = SimplifiedCommand::new(&command.command, true);
if !command.command.is_empty()
&& !simplified_command.result.is_empty()
&& let Err(e) = statement.execute(named_params! {
":cmd": &command.command,
":cmd_tpl": &simplified_command.result.clone(),
":session_id": &"IMPORTED",
":when_run": &command.when,
":exit_code": &0,
":selected": &0,
})
{
println!(
"A single history line could not be saved due to '{}' (command was '{}'), but other inserts should be fine.",
e, &command.command
);
}
}
}
}
transaction
.commit()
.unwrap_or_else(|err| panic!("McFly error: Unable to commit transaction: ({err})"));
schema::first_time_setup(&connection);
println!("done.");
History {
connection,
network: Network::default(),
}
}
fn from_db_path(path: PathBuf) -> History {
let connection = Connection::open(path)
.unwrap_or_else(|err| panic!("McFly error: Unable to open history database ({err})"));
db_extensions::add_db_functions(&connection);
History {
connection,
network: Network::default(),
}
}
}
================================================
FILE: src/history/mod.rs
================================================
pub use self::history::{Command, DumpCommand, Features, History};
mod db_extensions;
mod history;
mod schema;
================================================
FILE: src/history/schema.rs
================================================
use crate::simplified_command::SimplifiedCommand;
use rusqlite::{Connection, named_params};
use std::io;
use std::io::Write;
pub const CURRENT_SCHEMA_VERSION: u16 = 3;
pub fn first_time_setup(connection: &Connection) {
make_schema_versions_table(connection);
write_current_schema_version(connection);
}
pub fn migrate(connection: &Connection) {
make_schema_versions_table(connection);
let current_version: u16 = connection
.query_row::<Option<u16>, _, _>(
"select max(version) FROM schema_versions ORDER BY version DESC LIMIT 1",
[],
|row| row.get(0),
)
.unwrap_or_else(|err| panic!("McFly error: Query to work ({err})"))
.unwrap_or(0);
assert!(
current_version <= CURRENT_SCHEMA_VERSION,
"McFly error: Database schema version ({current_version}) is newer than the max version supported by this binary ({CURRENT_SCHEMA_VERSION}). You should update mcfly.",
);
if current_version < CURRENT_SCHEMA_VERSION {
print!("McFly: Upgrading McFly DB to version {CURRENT_SCHEMA_VERSION}, please wait...");
io::stdout()
.flush()
.unwrap_or_else(|err| panic!("McFly error: STDOUT flush should work ({err})"));
}
if current_version < 1 {
connection
.execute_batch(
"ALTER TABLE commands ADD COLUMN cmd_tpl TEXT; UPDATE commands SET cmd_tpl = '';",
)
.unwrap_or_else(|err| panic!("McFly error: Unable to add cmd_tpl to commands ({err})"));
let mut statement = connection
.prepare("UPDATE commands SET cmd_tpl = :cmd_tpl WHERE id = :id")
.unwrap_or_else(|err| panic!("McFly error: Unable to prepare update ({err})"));
for (id, cmd) in cmd_strings(connection) {
let simplified_command = SimplifiedCommand::new(cmd.as_str(), true);
statement
.execute(named_params! { ":cmd_tpl": &simplified_command.result, ":id": &id })
.unwrap_or_else(|err| panic!("McFly error: Insert to work ({err})"));
}
}
if current_version < 2 {
connection
.execute_batch(
"ALTER TABLE commands ADD COLUMN session_id TEXT; \
UPDATE commands SET session_id = 'UNKNOWN'; \
CREATE INDEX command_session_id ON commands (session_id);",
)
.unwrap_or_else(|err| {
panic!("McFly error: Unable to add session_id to commands ({err})")
});
}
if current_version < 3 {
connection
.execute_batch(
"CREATE TABLE selected_commands( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
cmd TEXT NOT NULL, \
session_id TEXT NOT NULL, \
dir TEXT NOT NULL \
); \
CREATE INDEX selected_command_session_cmds ON selected_commands (session_id, cmd); \
\
ALTER TABLE commands ADD COLUMN selected INTEGER; \
UPDATE commands SET selected = 0;",
)
.unwrap_or_else(|err| panic!("McFly error: Unable to add selected_commands ({err})"));
}
if current_version < CURRENT_SCHEMA_VERSION {
println!("done.");
write_current_schema_version(connection);
}
}
fn make_schema_versions_table(connection: &Connection) {
connection
.execute_batch(
"CREATE TABLE IF NOT EXISTS schema_versions( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
version INTEGER NOT NULL, \
when_run INTEGER NOT NULL);
CREATE UNIQUE INDEX IF NOT EXISTS schema_versions_index ON schema_versions (version);",
)
.unwrap_or_else(|err| {
panic!("McFly error: Unable to create schema_versions db table ({err})")
});
}
fn write_current_schema_version(connection: &Connection) {
let insert = format!(
"INSERT INTO schema_versions (version, when_run) VALUES ({CURRENT_SCHEMA_VERSION}, strftime('%s','now'))"
);
connection
.execute_batch(&insert)
.unwrap_or_else(|err| panic!("McFly error: Unable to update schema_versions ({err})"));
}
fn cmd_strings(connection: &Connection) -> Vec<(i64, String)> {
let query = "SELECT id, cmd FROM commands ORDER BY id DESC";
let mut statement = connection.prepare(query).unwrap();
let command_iter = statement
.query_map([], |row| Ok((row.get_unwrap(0), row.get_unwrap(1))))
.unwrap_or_else(|err| panic!("McFly error: Query Map to work ({err})"));
let mut vec = Vec::new();
for command in command_iter.flatten() {
vec.push(command);
}
vec
}
================================================
FILE: src/history_cleaner.rs
================================================
use crate::history::History;
use crate::settings::{HistoryFormat, Settings};
use crate::shell_history;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
pub fn clean(settings: &Settings, history: &History, command: &str) {
// Clean up the database.
history.delete_command(command);
match settings.history_format {
HistoryFormat::Bash | HistoryFormat::Zsh { .. } => {
// Clean up the contents of MCFLY_HISTORY and all other temporary history files in the same
// directory.
clean_temporary_files(&settings.mcfly_history, settings.history_format, command);
// Clean up HISTFILE/MCFLY_HISTFILE.
let histfile = PathBuf::from(
env::var("HISTFILE")
.or_else(|_| env::var("MCFLY_HISTFILE"))
.unwrap_or_else(|err| {
panic!(
"McFly error: Please ensure that HISTFILE/MCFLY_HISTFILE is set ({err})"
)
}),
);
shell_history::delete_lines(&histfile, settings.history_format, command);
}
// Fish integration does not use a MCFLY_HISTORY file because we can get the last command
// during a fish_postexec function.
// Also, deletion from the fish history file is done by fish itself, via commands sent out
// in the results file and interpreted by mcfly.fish.
HistoryFormat::Fish => {}
}
}
fn clean_temporary_files(mcfly_history: &Path, history_format: HistoryFormat, command: &str) {
let path = mcfly_history;
if let Some(directory) = path.parent() {
let expanded_path = fs::canonicalize(directory).unwrap_or_else(|err| {
panic!("McFly error: The contents of $MCFLY_HISTORY appear invalid ({err})")
});
let paths = fs::read_dir(expanded_path).unwrap();
for entry in paths.flatten() {
if let Some(file_name) = entry.path().file_name()
&& let Some(valid_unicode_str) = file_name.to_str()
&& valid_unicode_str.starts_with("mcfly.")
{
shell_history::delete_lines(&entry.path(), history_format, command);
}
}
}
}
================================================
FILE: src/init.rs
================================================
use super::settings::InitMode;
use std::env;
pub struct Init {}
impl Init {
pub fn new(init_mode: &InitMode) -> Self {
match init_mode {
InitMode::Bash => {
Init::init_bash();
}
InitMode::Zsh => {
Init::init_zsh();
}
InitMode::Fish => {
Init::init_fish();
}
InitMode::Powershell => {
Init::init_pwsh();
}
}
Self {}
}
pub fn init_bash() {
let script = include_str!("../mcfly.bash");
print!("{script}");
}
pub fn init_zsh() {
let script = include_str!("../mcfly.zsh");
print!("{script}");
}
pub fn init_fish() {
let script = include_str!("../mcfly.fish");
print!("{script}");
}
pub fn init_pwsh() {
let script = include_str!("../mcfly.ps1")
.replace("::MCFLY::", env::current_exe().unwrap().to_str().unwrap());
print!("{script}");
}
}
================================================
FILE: src/interface.rs
================================================
use crate::command_input::{CommandInput, Move};
use crate::history::History;
use crate::fixed_length_grapheme_string::FixedLengthGraphemeString;
use crate::history::Command;
use crate::history_cleaner;
use crate::settings::{InterfaceView, KeyScheme, ResultFilter};
use crate::settings::{ResultSort, Settings};
use chrono::{Duration, TimeZone, Utc};
use crossterm::event::KeyCode::Char;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, read};
use crossterm::style::{Color, Print, SetBackgroundColor, SetForegroundColor};
use crossterm::terminal::{self, LeaveAlternateScreen};
use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen};
use crossterm::{cursor, execute, queue};
use humantime::format_duration;
use std::io::{Write, stdout};
use std::string::String;
pub struct Interface<'a> {
history: &'a History,
settings: &'a Settings,
input: CommandInput,
selection: usize,
matches: Vec<Command>,
debug: bool,
run: bool,
delete_requests: Vec<String>,
menu_mode: MenuMode,
in_vim_insert_mode: bool,
result_sort: ResultSort,
result_filter: ResultFilter,
}
pub struct SelectionResult {
/// Whether the user requested to run the resulting command immediately.
pub run: bool,
/// The command string the user selected, if any.
pub selection: Option<String>,
/// Commands the user has requested be deleted from shell history.
pub delete_requests: Vec<String>,
}
pub enum MoveSelection {
Up,
Down,
}
#[derive(PartialEq, Eq)]
pub enum MenuMode {
Normal,
ConfirmDelete,
}
impl MenuMode {
fn text(&self, interface: &Interface) -> String {
let mut menu_text = String::from("McFly");
match *self {
MenuMode::Normal => match interface.settings.key_scheme {
KeyScheme::Emacs => menu_text.push_str(" | ESC - Exit | "),
KeyScheme::Vim => {
if interface.in_vim_insert_mode {
menu_text.push_str(" (Ins) | ESC - Cmd | ");
} else {
menu_text.push_str(" (Cmd) | ESC - Exit | ");
}
}
},
MenuMode::ConfirmDelete => {
return String::from("Delete selected command from the history? (Y/N)");
}
}
if interface.settings.disable_run_command {
menu_text.push_str("⏎, TAB - Edit | ");
} else {
menu_text.push_str("⏎ - Run | TAB - Edit | ");
}
match interface.result_sort {
ResultSort::Rank => menu_text.push_str("F1 - Rank Sort | "),
ResultSort::LastRun => menu_text.push_str("F1 - Time Sort | "),
}
menu_text.push_str("F2 - Delete | ");
match interface.result_filter {
ResultFilter::Global => menu_text.push_str("F3 - All Directories"),
ResultFilter::CurrentDirectory => menu_text.push_str("F3 - This Directory"),
}
menu_text
}
fn bg(&self, normal: Color) -> Color {
match *self {
MenuMode::Normal => normal,
MenuMode::ConfirmDelete => Color::Red,
}
}
}
const PROMPT_LINE_INDEX: u16 = 3;
const INFO_LINE_INDEX: u16 = 1;
const RESULTS_TOP_INDEX: u16 = 5;
impl<'a> Interface<'a> {
pub fn new(settings: &'a Settings, history: &'a History) -> Interface<'a> {
Interface {
history,
settings,
input: CommandInput::from(settings.command.clone()),
selection: 0,
matches: Vec::new(),
debug: settings.debug,
run: false,
delete_requests: Vec::new(),
menu_mode: MenuMode::Normal,
in_vim_insert_mode: true,
result_sort: settings.result_sort.clone(),
result_filter: settings.result_filter.clone(),
}
}
pub fn display(&mut self) -> SelectionResult {
self.build_cache_table();
self.select();
let command = self.input.command.clone();
if command.chars().any(|c| !c.is_whitespace()) {
self.history.record_selected_from_ui(
&command,
&self.settings.session_id,
&self.settings.dir,
);
SelectionResult {
run: self.run,
selection: Some(command),
// Remove delete_requests from the Interface, in case it's used to display() again.
delete_requests: self.delete_requests.split_off(0),
}
} else {
SelectionResult {
run: self.run,
selection: None,
delete_requests: self.delete_requests.split_off(0),
}
}
}
fn build_cache_table(&self) {
self.history.build_cache_table(
&self.settings.dir.clone(),
&self.result_filter,
&Some(self.settings.session_id.clone()),
None,
None,
None,
self.settings.limit,
);
}
fn menubar<W: Write>(&self, screen: &mut W) {
if !self.settings.disable_menu {
let (width, _height): (u16, u16) = terminal::size().unwrap();
queue!(
screen,
cursor::Hide,
cursor::MoveTo(0, self.info_line_index()),
Clear(ClearType::CurrentLine),
SetBackgroundColor(self.menu_mode.bg(self.settings.colors.menubar_bg)),
SetForegroundColor(self.settings.colors.menubar_fg),
cursor::MoveTo(1, self.info_line_index()),
Print(format!(
"{text:width$}",
text = self.menu_mode.text(self),
width = width as usize - 1
)),
SetBackgroundColor(Color::Reset)
)
.unwrap();
}
}
fn prompt<W: Write>(&self, screen: &mut W) {
let prompt_line_index = self.prompt_line_index();
let fg = if self.settings.lightmode {
self.settings.colors.lightmode_colors.prompt
} else {
self.settings.colors.darkmode_colors.prompt
};
queue!(
screen,
cursor::MoveTo(1, prompt_line_index),
SetForegroundColor(fg),
Clear(ClearType::CurrentLine),
Print(format!("{} {}", self.settings.prompt, self.input)),
cursor::MoveTo(self.input.cursor as u16 + 3, prompt_line_index),
cursor::Show
)
.unwrap();
}
fn debug_cursor<W: Write>(&self, screen: &mut W) {
let result_top_index = self.result_top_index();
queue!(
screen,
cursor::Hide,
cursor::MoveTo(0, result_top_index + self.settings.results + 1)
)
.unwrap();
}
fn results<W: Write>(&mut self, screen: &mut W) {
let result_top_index = self.result_top_index();
queue!(screen, cursor::Hide, cursor::MoveTo(1, result_top_index)).unwrap();
let (width, height): (u16, u16) = terminal::size().unwrap();
let result_height = (height - RESULTS_TOP_INDEX) as usize
+ if self.is_screen_view_bottom() { 1 } else { 0 };
if !self.matches.is_empty() && self.selection > self.matches.len() - 1 {
self.selection = self.matches.len() - 1;
}
let mut index = 0;
let in_page = self.selection < result_height;
let view_range = if in_page {
let len = result_height.min(self.matches.len());
&self.matches[..len]
} else {
let offset = self.selection - result_height + 1;
let len = (offset + result_height).min(self.matches.len());
&self.matches[offset..len]
};
for command in view_range {
let mut fg = if self.settings.lightmode {
self.settings.colors.lightmode_colors.results_fg
} else {
self.settings.colors.darkmode_colors.results_fg
};
let mut highlight = if self.settings.lightmode {
self.settings.colors.lightmode_colors.results_hl
} else {
self.settings.colors.darkmode_colors.results_hl
};
let mut bg = Color::Reset;
if index == self.selection.min(result_height - 1) {
if self.settings.lightmode {
fg = self.settings.colors.lightmode_colors.results_selection_fg;
bg = self.settings.colors.lightmode_colors.results_selection_bg;
highlight = self.settings.colors.lightmode_colors.results_selection_hl;
} else {
fg = self.settings.colors.darkmode_colors.results_selection_fg;
bg = self.settings.colors.darkmode_colors.results_selection_bg;
highlight = self.settings.colors.darkmode_colors.results_selection_hl;
}
}
let command_line_index = self.command_line_index(index as i16);
queue!(
screen,
cursor::MoveTo(1, (command_line_index + result_top_index as i16) as u16),
Clear(ClearType::CurrentLine),
SetBackgroundColor(bg),
SetForegroundColor(fg),
Print(Interface::truncate_for_display(
command, width, highlight, fg, self.debug
))
)
.unwrap();
if let Some(last_run) = command.last_run {
queue!(
screen,
cursor::MoveTo(
width - 9,
(command_line_index + result_top_index as i16) as u16
)
)
.unwrap();
let duration = &format_duration(
Duration::minutes(
Utc::now()
.signed_duration_since(Utc.timestamp_opt(last_run, 0).unwrap())
.num_minutes(),
)
.to_std()
.unwrap(),
)
.to_string()
.split(' ')
.take(2)
.map(|s| {
s.replace("years", "y")
.replace("year", "y")
.replace("months", "mo")
.replace("month", "mo")
.replace("days", "d")
.replace("day", "d")
.replace("hours", "h")
.replace("hour", "h")
.replace("minutes", "m")
.replace("minute", "m")
.replace("0s", "< 1m")
})
.collect::<Vec<String>>()
.join(" ");
let timing_color = if self.settings.lightmode {
self.settings.colors.lightmode_colors.timing
} else {
self.settings.colors.darkmode_colors.timing
};
queue!(
screen,
cursor::MoveTo(
width - 9,
(command_line_index + self.result_top_index() as i16) as u16
),
SetForegroundColor(timing_color),
Print(format!("{duration:>9}")),
SetForegroundColor(Color::Reset),
SetBackgroundColor(Color::Reset)
)
.unwrap();
}
index += 1;
}
// Since we only clear by line instead of clearing the screen each update,
// we need to clear all the lines that may have previously had a command
for i in index..result_height {
let command_line_index = self.command_line_index(i as i16);
queue!(
screen,
cursor::MoveTo(1, (command_line_index + result_top_index as i16) as u16),
Clear(ClearType::CurrentLine)
)
.unwrap();
}
}
#[allow(unused)]
fn debug<W: Write, S: Into<String>>(&self, screen: &mut W, s: S) {
queue!(
screen,
cursor::MoveTo(0, 0),
Clear(ClearType::CurrentLine),
Print(s.into())
)
.unwrap();
screen.flush().unwrap();
}
fn move_selection(&mut self, direction: MoveSelection) {
if self.is_screen_view_bottom() {
match direction {
MoveSelection::Up => {
self.selection += 1;
}
MoveSelection::Down => {
if self.selection > 0 {
self.selection -= 1;
}
}
}
} else {
match direction {
MoveSelection::Up => {
if self.selection > 0 {
self.selection -= 1;
}
}
MoveSelection::Down => {
self.selection += 1;
}
}
}
}
fn accept_selection(&mut self) {
if !self.matches.is_empty() {
self.input.set(&self.matches[self.selection].cmd);
}
}
fn confirm(&mut self, confirmation: bool) {
if confirmation && let MenuMode::ConfirmDelete = self.menu_mode {
self.delete_selection();
}
self.menu_mode = MenuMode::Normal;
}
fn delete_selection(&mut self) {
if !self.matches.is_empty() {
{
let command = &self.matches[self.selection];
history_cleaner::clean(self.settings, self.history, &command.cmd);
self.delete_requests.push(command.cmd.clone());
}
self.build_cache_table();
self.refresh_matches(false);
}
}
fn refresh_matches(&mut self, reset_selection: bool) {
if reset_selection {
self.selection = 0;
}
self.matches = self.history.find_matches(
&self.input.command,
self.settings.results as i16,
self.settings.fuzzy,
&self.result_sort,
);
}
fn switch_result_sort(&mut self) {
match self.result_sort {
ResultSort::Rank => self.result_sort = ResultSort::LastRun,
ResultSort::LastRun => self.result_sort = ResultSort::Rank,
}
}
fn switch_result_filter(&mut self) {
self.result_filter = match self.result_filter {
ResultFilter::Global => ResultFilter::CurrentDirectory,
ResultFilter::CurrentDirectory => ResultFilter::Global,
};
self.build_cache_table();
}
fn select(&mut self) {
let mut screen = stdout();
terminal::enable_raw_mode().unwrap();
queue!(screen, EnterAlternateScreen, Clear(ClearType::All)).unwrap();
self.refresh_matches(true);
self.results(&mut screen);
self.menubar(&mut screen);
self.prompt(&mut screen);
screen.flush().unwrap();
loop {
let event =
read().unwrap_or_else(|e| panic!("McFly error: failed to read input {:?}", &e));
self.debug_cursor(&mut screen);
match self.menu_mode {
MenuMode::Normal => {
let early_out = match self.settings.key_scheme {
KeyScheme::Emacs => self.select_with_emacs_key_scheme(event),
KeyScheme::Vim => {
if let Event::Key(key_event) = event {
self.select_with_vim_key_scheme(key_event)
} else {
false
}
}
};
if early_out {
break;
}
}
MenuMode::ConfirmDelete => {
if let Event::Key(key_event) = event {
match key_event {
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('c' | 'd' | 'g' | 'z' | 'r'),
..
} => {
self.run = false;
self.input.clear();
break;
}
KeyEvent {
code: Char('y' | 'Y'),
..
} => {
self.confirm(true);
}
KeyEvent {
code: Char('n' | 'N') | KeyCode::Esc,
..
} => {
self.confirm(false);
}
_ => {}
}
}
}
}
self.results(&mut screen);
self.menubar(&mut screen);
self.prompt(&mut screen);
screen.flush().unwrap();
}
queue!(
screen,
Clear(ClearType::All),
cursor::Show,
LeaveAlternateScreen
)
.unwrap();
terminal::disable_raw_mode().unwrap();
}
fn select_with_emacs_key_scheme(&mut self, event: Event) -> bool {
match event {
Event::Key(event) => self.handle_emacs_keyevent(event),
Event::Paste(s) => {
for i in s.chars() {
self.input.insert(i);
}
self.refresh_matches(true);
false
}
_ => false,
}
}
fn handle_emacs_keyevent(&mut self, event: KeyEvent) -> bool {
if event.kind != KeyEventKind::Press {
return false;
}
match event {
KeyEvent {
code: KeyCode::Enter | Char('\r' | '\n'),
..
}
| KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('j'),
..
} => {
self.run = !self.settings.disable_run_command;
self.accept_selection();
return true;
}
KeyEvent {
code: KeyCode::Tab, ..
} => {
self.run = false;
self.accept_selection();
return true;
}
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('c' | 'g' | 'z' | 'r'),
..
}
| KeyEvent {
code: KeyCode::Esc, ..
} => {
self.run = false;
self.input.clear();
return true;
}
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code,
..
} => match code {
Char('b') => self.input.move_cursor(Move::Backward),
Char('f') => self.input.move_cursor(Move::Forward),
Char('a') => self.input.move_cursor(Move::BOL),
Char('e') => self.input.move_cursor(Move::EOL),
Char('v') => self.debug = !self.debug,
Char('k') => {
self.input.delete(Move::EOL);
self.refresh_matches(true);
}
Char('u') => {
self.input.delete(Move::BOL);
self.refresh_matches(true);
}
Char('w') => {
self.input.delete(Move::BackwardWord);
self.refresh_matches(true);
}
Char('p') => self.move_selection(MoveSelection::Up),
Char('n') => self.move_selection(MoveSelection::Down),
Char('h') => {
self.input.delete(Move::Backward);
self.refresh_matches(true);
}
Char('d') => {
self.input.delete(Move::Forward);
self.refresh_matches(true);
}
_ => {}
},
KeyEvent {
modifiers: KeyModifiers::ALT,
code: Char('\x08' | '\x7f'),
..
} => {
self.input.delete(Move::BackwardWord);
self.refresh_matches(true);
}
KeyEvent {
modifiers: KeyModifiers::ALT,
code,
..
} => match code {
Char('b') => self.input.move_cursor(Move::BackwardWord),
Char('f') => self.input.move_cursor(Move::ForwardWord),
Char('d') => {
self.input.delete(Move::ForwardWord);
self.refresh_matches(true);
}
_ => {}
},
KeyEvent {
code: KeyCode::Left,
..
} => self.input.move_cursor(Move::Backward),
KeyEvent {
code: KeyCode::Right,
..
} => self.input.move_cursor(Move::Forward),
KeyEvent {
code: KeyCode::Up | KeyCode::PageUp,
..
} => self.move_selection(MoveSelection::Up),
KeyEvent {
code: KeyCode::Down | KeyCode::PageDown,
..
} => self.move_selection(MoveSelection::Down),
KeyEvent {
code: KeyCode::Backspace,
..
} => {
self.input.delete(Move::Backward);
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::Delete,
..
} => {
self.input.delete(Move::Forward);
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::Home,
..
} => self.input.move_cursor(Move::BOL),
KeyEvent {
code: KeyCode::End, ..
} => self.input.move_cursor(Move::EOL),
KeyEvent { code: Char(c), .. } => {
self.input.insert(c);
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::F(1),
..
} => {
self.switch_result_sort();
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::F(2),
..
} => {
if !self.matches.is_empty() {
if self.settings.delete_without_confirm {
self.delete_selection();
} else {
self.menu_mode = MenuMode::ConfirmDelete;
}
}
}
KeyEvent {
code: KeyCode::F(3),
..
} => {
self.switch_result_filter();
self.refresh_matches(true);
}
_ => {}
}
false
}
fn select_with_vim_key_scheme(&mut self, event: KeyEvent) -> bool {
if event.kind != KeyEventKind::Press {
return false;
}
if self.in_vim_insert_mode {
match event {
KeyEvent {
code: KeyCode::Tab, ..
} => {
self.run = false;
self.accept_selection();
return true;
}
KeyEvent {
code: KeyCode::Enter,
..
}
| KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('j'),
..
} => {
self.run = !self.settings.disable_run_command;
self.accept_selection();
return true;
}
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('c' | 'g' | 'z' | 'r'),
..
} => {
self.run = false;
self.input.clear();
return true;
}
KeyEvent {
code: KeyCode::Left,
..
} => self.input.move_cursor(Move::Backward),
KeyEvent {
code: KeyCode::Right,
..
} => self.input.move_cursor(Move::Forward),
KeyEvent {
code: KeyCode::Up | KeyCode::PageUp,
..
}
| KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('u' | 'p'),
..
} => self.move_selection(MoveSelection::Up),
KeyEvent {
code: KeyCode::Down | KeyCode::PageDown,
..
}
| KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('d' | 'n'),
..
} => self.move_selection(MoveSelection::Down),
KeyEvent {
code: KeyCode::Esc, ..
} => self.in_vim_insert_mode = false,
KeyEvent {
code: KeyCode::Backspace,
..
} => {
self.input.delete(Move::Backward);
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::Delete,
..
} => {
self.input.delete(Move::Forward);
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::Home,
..
} => self.input.move_cursor(Move::BOL),
KeyEvent {
code: KeyCode::End, ..
} => self.input.move_cursor(Move::EOL),
KeyEvent { code: Char(c), .. } => {
self.input.insert(c);
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::F(1),
..
} => {
self.switch_result_sort();
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::F(2),
..
} => {
if !self.matches.is_empty() {
if self.settings.delete_without_confirm {
self.delete_selection();
} else {
self.menu_mode = MenuMode::ConfirmDelete;
}
}
}
KeyEvent {
code: KeyCode::F(3),
..
} => {
self.switch_result_filter();
self.refresh_matches(true);
}
_ => {}
}
} else {
match event {
KeyEvent {
code: KeyCode::Tab, ..
} => {
self.run = false;
self.accept_selection();
return true;
}
KeyEvent {
code: KeyCode::Enter,
..
}
| KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('j'),
..
} => {
self.run = !self.settings.disable_run_command;
self.accept_selection();
return true;
}
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('c' | 'g' | 'z' | 'r'),
..
}
| KeyEvent {
code: KeyCode::Esc, ..
} => {
self.run = false;
self.input.clear();
return true;
}
KeyEvent {
code: KeyCode::Left | Char('h'),
..
} => self.input.move_cursor(Move::Backward),
KeyEvent {
code: KeyCode::Right | Char('l'),
..
} => self.input.move_cursor(Move::Forward),
KeyEvent {
code: KeyCode::Up | KeyCode::PageUp | Char('k'),
..
}
| KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('u'),
..
} => self.move_selection(MoveSelection::Up),
KeyEvent {
code: KeyCode::Down | KeyCode::PageDown | Char('j'),
..
}
| KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: Char('d'),
..
} => self.move_selection(MoveSelection::Down),
KeyEvent {
code: Char('b' | 'e'),
..
} => self.input.move_cursor(Move::BackwardWord),
KeyEvent {
code: Char('w'), ..
} => self.input.move_cursor(Move::ForwardWord),
KeyEvent {
code: Char('0' | '^'),
..
} => self.input.move_cursor(Move::BOL),
KeyEvent {
code: Char('$'), ..
} => self.input.move_cursor(Move::EOL),
KeyEvent {
code: Char('i' | 'a'),
..
} => self.in_vim_insert_mode = true,
KeyEvent {
code: KeyCode::Backspace,
..
} => {
self.input.delete(Move::Backward);
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::Delete | Char('x'),
..
} => {
self.input.delete(Move::Forward);
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::Home,
..
} => self.input.move_cursor(Move::BOL),
KeyEvent {
code: KeyCode::End, ..
} => self.input.move_cursor(Move::EOL),
KeyEvent {
code: KeyCode::F(1),
..
} => {
self.switch_result_sort();
self.refresh_matches(true);
}
KeyEvent {
code: KeyCode::F(2),
..
} => {
if !self.matches.is_empty() {
if self.settings.delete_without_confirm {
self.delete_selection();
} else {
self.menu_mode = MenuMode::ConfirmDelete;
}
}
}
KeyEvent {
code: KeyCode::F(3),
..
} => {
self.switch_result_filter();
self.refresh_matches(true);
}
_ => {}
}
}
false
}
fn truncate_for_display(
command: &Command,
width: u16,
highlight_color: Color,
base_color: Color,
debug: bool,
) -> String {
let debug_space = if debug { 90 } else { 0 };
let max_grapheme_length = if width > debug_space {
width - debug_space - 9
} else {
11
};
let mut out = FixedLengthGraphemeString::empty(max_grapheme_length);
let mut match_indices = command.match_indices.iter().peekable();
for (i, c) in command.cmd.char_indices() {
match match_indices.peek() {
Some(&&j) if i == j => {
let _ = match_indices.next();
execute!(out, SetForegroundColor(highlight_color)).unwrap();
out.push_grapheme_str(c);
}
_ => {
execute!(out, SetForegroundColor(base_color)).unwrap();
out.push_grapheme_str(c);
}
}
}
if debug {
out.max_grapheme_length += debug_space;
out.push_grapheme_str(" ");
execute!(out, SetForegroundColor(Color::Blue)).unwrap();
out.push_grapheme_str(format!("rnk: {:.*} ", 2, command.rank));
out.push_grapheme_str(format!("age: {:.*} ", 2, command.features.age_factor));
out.push_grapheme_str(format!("lng: {:.*} ", 2, command.features.length_factor));
out.push_grapheme_str(format!("ext: {:.*} ", 0, command.features.exit_factor));
out.push_grapheme_str(format!(
"r_ext: {:.*} ",
0, command.features.recent_failure_factor
));
out.push_grapheme_str(format!("dir: {:.*} ", 3, command.features.dir_factor));
out.push_grapheme_str(format!(
"s_dir: {:.*} ",
3, command.features.selected_dir_factor
));
out.push_grapheme_str(format!("ovlp: {:.*} ", 3, command.features.overlap_factor));
out.push_grapheme_str(format!(
"i_ovlp: {:.*} ",
3, command.features.immediate_overlap_factor
));
out.push_grapheme_str(format!(
"occ: {:.*}",
2, command.features.occurrences_factor
));
out.push_grapheme_str(format!(
"s_occ: {:.*} ",
2, command.features.selected_occurrences_factor
));
execute!(out, SetForegroundColor(base_color)).unwrap();
}
out.string
}
fn result_top_index(&self) -> u16 {
let (_width, height): (u16, u16) = terminal::size().unwrap();
if self.is_screen_view_bottom() {
return height - RESULTS_TOP_INDEX;
}
RESULTS_TOP_INDEX
}
fn prompt_line_index(&self) -> u16 {
let (_width, height): (u16, u16) = terminal::size().unwrap();
if self.is_screen_view_bottom() {
return height - PROMPT_LINE_INDEX;
}
PROMPT_LINE_INDEX
}
fn info_line_index(&self) -> u16 {
let (_width, height): (u16, u16) = terminal::size().unwrap();
if self.is_screen_view_bottom() {
return height;
}
INFO_LINE_INDEX
}
fn command_line_index(&self, index: i16) -> i16 {
if self.is_screen_view_bottom() {
return -index;
}
index
}
fn is_screen_view_bottom(&self) -> bool {
self.settings.interface_view == InterfaceView::Bottom
}
}
// TODO:
// Ctrl('X') + Ctrl('U') => undo
// Ctrl('X') + Ctrl('G') => abort
// Meta('c') => capitalize word
// Meta('l') => downcase word
// Meta('t') => transpose words
// Meta('u') => upcase word
// Meta('y') => yank pop
// Ctrl('r') => reverse history search
// Ctrl('s') => forward history search
// Ctrl('t') => transpose characters
// Ctrl('q') | Ctrl('v') => quoted insert
// Ctrl('y') => yank
// Ctrl('_') => undo
================================================
FILE: src/lib.rs
================================================
pub mod cli;
pub mod command_input;
pub mod dumper;
pub mod fake_typer;
pub mod fixed_length_grapheme_string;
pub mod history;
pub mod history_cleaner;
pub mod init;
pub mod interface;
pub mod network;
pub mod node;
pub mod path_update_helpers;
pub mod settings;
pub mod shell_history;
pub mod simplified_command;
pub mod stats_generator;
pub mod time;
pub mod trainer;
pub mod training_cache;
pub mod training_sample_generator;
================================================
FILE: src/main.rs
================================================
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use mcfly::dumper::Dumper;
use mcfly::fake_typer;
use mcfly::history::History;
use mcfly::init::Init;
use mcfly::interface::Interface;
use mcfly::settings::Mode;
use mcfly::settings::Settings;
use mcfly::shell_history;
use mcfly::stats_generator::StatsGenerator;
use mcfly::trainer::Trainer;
fn handle_addition(settings: &Settings) {
let history = History::load(settings.history_format);
if history.should_add(&settings.command) {
history.add(
&settings.command,
&settings.session_id,
&settings.dir,
&settings.when_run,
settings.exit_code,
&settings.old_dir,
);
if let Some(append_to_histfile) = &settings.append_to_histfile {
let histfile = PathBuf::from(append_to_histfile);
let command = shell_history::HistoryCommand::new(
&settings.command,
settings.when_run.unwrap_or(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|err| panic!("McFly error: Time went backwards ({err})"))
.as_secs() as i64,
),
settings.history_format,
);
shell_history::append_history_entry(&command, &histfile, settings.debug);
}
}
}
fn handle_search(settings: &Settings) {
let history = History::load(settings.history_format);
let result = Interface::new(settings, &history).display();
if let Some(cmd) = result.selection {
if let Some(path) = &settings.output_selection {
// Output selection results to a file.
let mut out: String = String::new();
// First we say the desired mode, depending on the key pressed by the user - simply
// displaying the selected command, or running it.
if result.run {
out.push_str("mode run\n");
} else {
out.push_str("mode display\n");
}
// Next, the desired commandline selected by the user.
out.push_str("commandline ");
out.push_str(&cmd);
out.push('\n');
// Finally, any requests for deletion of commands from shell history, for cases where
// shells need to handle this natively instead of through us editing HISTFILE/MCFLY_HISTFILE.
for delete_request in result.delete_requests {
out.push_str("delete ");
out.push_str(&delete_request);
out.push('\n');
}
fs::write(path, &out)
.unwrap_or_else(|err| panic!("McFly error: unable to write to {path}: {err}"));
} else {
fake_typer::use_tiocsti(&cmd);
if result.run {
fake_typer::use_tiocsti("\n");
}
}
}
}
fn handle_train(settings: &Settings) {
let mut history = History::load(settings.history_format);
Trainer::new(settings, &mut history).train();
}
fn handle_move(settings: &Settings) {
let history = History::load(settings.history_format);
history.update_paths(&settings.old_dir.clone().unwrap(), &settings.dir, true);
}
fn handle_init(settings: &Settings) {
Init::new(&settings.init_mode);
}
fn handle_dump(settings: &Settings) {
let history = History::load(settings.history_format);
Dumper::new(settings, &history).dump();
}
fn handle_stats(settings: &Settings) {
let history = History::load(settings.history_format);
let stats = StatsGenerator::new(&history).generate_stats(settings);
println!("{stats}");
}
fn main() {
let mut settings = Settings::parse_args();
settings.load_config();
match settings.mode {
Mode::Add => {
handle_addition(&settings);
}
Mode::Search => {
handle_search(&settings);
}
Mode::Train => {
handle_train(&settings);
}
Mode::Move => {
handle_move(&settings);
}
Mode::Init => {
handle_init(&settings);
}
Mode::Dump => {
handle_dump(&settings);
}
Mode::Stats => handle_stats(&settings),
}
}
================================================
FILE: src/network.rs
================================================
#![allow(clippy::unreadable_literal)]
use crate::history::Features;
use crate::node::Node;
use crate::training_sample_generator::TrainingSampleGenerator;
use rand::Rng;
#[derive(Debug, Copy, Clone)]
pub struct Network {
pub final_bias: f64,
pub final_weights: [f64; 3],
pub final_sum: f64,
pub final_output: f64,
pub hidden_nodes: [Node; 3],
pub hidden_node_sums: [f64; 3],
pub hidden_node_outputs: [f64; 3],
}
impl Default for Network {
fn default() -> Network {
Network {
final_bias: -0.3829333755179377,
final_weights: [0.44656858145177714, -1.9550439349609872, -2.963322601316632],
final_sum: 0.0,
final_output: 0.0,
hidden_nodes: [
Node {
offset: -0.878184962836099,
age: -0.9045522440219468,
length: 0.5406937685800283,
exit: -0.3472765681766297,
recent_failure: -0.05291342121445077,
selected_dir: -0.35027519196134,
dir: -0.2466069217936986,
overlap: 0.4791784213482642,
immediate_overlap: 0.5565797758340211,
selected_occurrences: -0.3600203296209723,
occurrences: 0.15694312742881805,
},
Node {
offset: -0.04362945902379799,
age: -0.25381913331319716,
length: 0.4238780143901607,
exit: 0.21906785628210726,
recent_failure: -0.9510136025685453,
selected_dir: -0.04654084670567356,
dir: -2.2858050301068693,
overlap: -0.562274365705918,
immediate_overlap: -0.47252489212451904,
selected_occurrences: 0.2446391951417497,
occurrences: -1.4846489581676605,
},
Node {
offset: -0.11992725490486622,
age: 0.3759013420273308,
length: 1.674601413922965,
exit: -0.15529596916772864,
recent_failure: -0.7819181782432957,
selected_dir: -1.1890532332896768,
dir: 0.34723729558743677,
overlap: 0.09372412920642742,
immediate_overlap: 0.393989158881144,
selected_occurrences: -0.2383372126951215,
occurrences: -2.196219880265691,
},
],
hidden_node_sums: [0.0, 0.0, 0.0],
hidden_node_outputs: [0.0, 0.0, 0.0],
}
}
}
impl Network {
#[must_use]
pub fn random() -> Network {
let mut rng = rand::rng();
Network {
final_bias: rng.random_range(-1.0..1.0),
final_weights: [
rng.random_range(-1.0..1.0),
rng.random_range(-1.0..1.0),
rng.random_range(-1.0..1.0),
],
hidden_nodes: [Node::random(), Node::random(), Node::random()],
hidden_node_sums: [0.0, 0.0, 0.0],
hidden_node_outputs: [0.0, 0.0, 0.0],
final_sum: 0.0,
final_output: 0.0,
}
}
pub fn compute(&mut self, features: &Features) {
self.final_sum = self.final_bias;
for i in 0..self.hidden_nodes.len() {
self.hidden_node_sums[i] = self.hidden_nodes[i].dot(features);
self.hidden_node_outputs[i] = self.hidden_node_sums[i].tanh();
self.final_sum += self.hidden_node_outputs[i] * self.final_weights[i];
}
self.final_output = self.final_sum.tanh();
}
#[must_use]
pub fn dot(&self, features: &Features) -> f64 {
let mut network_output = self.final_bias;
for (node, output_weight) in self.hidden_nodes.iter().zip(self.final_weights.iter()) {
let node_output = node.output(features);
network_output += node_output * output_weight;
}
network_output
}
#[must_use]
pub fn output(&self, features: &Features) -> f64 {
self.dot(features).tanh()
}
#[must_use]
pub fn average_error(&self, generator: &TrainingSampleGenerator, records: usize) -> f64 {
let mut error = 0.0;
let mut samples = 0.0;
generator.generate(Some(records), |features: &Features, correct: bool| {
let target = if correct { 1.0 } else { -1.0 };
let output = self.output(features);
error += 0.5 * (target - output).powi(2);
samples += 1.0;
});
error / samples
}
}
================================================
FILE: src/node.rs
================================================
use crate::history::Features;
use rand::Rng;
use std::f64;
#[derive(Debug, Copy, Clone, Default)]
pub struct Node {
pub offset: f64,
pub age: f64,
pub length: f64,
pub exit: f64,
pub recent_failure: f64,
pub selected_dir: f64,
pub dir: f64,
pub overlap: f64,
pub immediate_overlap: f64,
pub selected_occurrences: f64,
pub occurrences: f64,
}
impl Node {
#[must_use]
pub fn random() -> Node {
let mut rng = rand::rng();
Node {
offset: rng.random_range(-1.0..1.0),
age: rng.random_range(-1.0..1.0),
length: rng.random_range(-1.0..1.0),
exit: rng.random_range(-1.0..1.0),
recent_failure: rng.random_range(-1.0..1.0),
selected_dir: rng.random_range(-1.0..1.0),
dir: rng.random_range(-1.0..1.0),
overlap: rng.random_range(-1.0..1.0),
immediate_overlap: rng.random_range(-1.0..1.0),
selected_occurrences: rng.random_range(-1.0..1.0),
occurrences: rng.random_range(-1.0..1.0),
}
}
#[must_use]
pub fn dot(&self, features: &Features) -> f64 {
self.offset
+ features.age_factor * self.age
+ features.length_factor * self.length
+ features.exit_factor * self.exit
+ features.recent_failure_factor * self.recent_failure
+ features.selected_dir_factor * self.selected_dir
+ features.dir_factor * self.dir
+ features.overlap_factor * self.overlap
+ features.immediate_overlap_factor * self.immediate_overlap
+ features.selected_occurrences_factor * self.selected_occurrences
+ features.occurrences_factor * self.occurrences
}
#[must_use]
pub fn output(&self, features: &Features) -> f64 {
self.dot(features).tanh()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dot() {
let node = Node {
offset: 0.0,
age: 0.0,
length: 0.0,
exit: 0.0,
recent_failure: 0.0,
selected_dir: 0.0,
dir: 0.0,
overlap: 0.0,
immediate_overlap: 0.0,
selected_occurrences: 0.0,
occurrences: 0.0,
};
let features = Features {
age_factor: 1.0,
length_factor: 1.0,
exit_factor: 1.0,
recent_failure_factor: 1.0,
selected_dir_factor: 1.0,
dir_factor: 1.0,
overlap_factor: 1.0,
immediate_overlap_factor: 1.0,
selected_occurrences_factor: 1.0,
occurrences_factor: 1.0,
};
assert_eq!(node.dot(&features), 0.0);
}
}
================================================
FILE: src/path_update_helpers.rs
================================================
use crate::settings::pwd;
use path_absolutize::Absolutize;
use std::path::Path;
use unicode_segmentation::UnicodeSegmentation;
#[must_use]
pub fn normalize_path(incoming_path: &str) -> String {
let expanded_path = shellexpand::tilde(incoming_path).to_string();
Path::new(&expanded_path)
.absolutize_from(pwd())
.unwrap()
.to_str()
.unwrap_or_else(|| panic!("McFly error: Path must be a valid UTF8 string"))
.to_string()
}
#[must_use]
pub fn parse_mv_command(command: &str) -> Vec<String> {
let mut in_double_quote = false;
let mut in_single_quote = false;
let mut escaped = false;
let mut buffer = String::new();
let mut result: Vec<String> = Vec::new();
for grapheme in command.graphemes(true) {
match grapheme {
"\\" => {
escaped = true;
}
"\"" => {
if escaped {
escaped = false;
buffer.push_str(grapheme);
} else if in_double_quote {
in_double_quote = false;
if !buffer.is_empty() {
result.push(buffer);
}
buffer = String::new();
} else if !in_single_quote {
in_double_quote = true;
} else {
buffer.push_str(grapheme);
}
}
"\'" => {
if in_single_quote {
in_single_quote = false;
if !buffer.is_empty() {
result.push(buffer);
}
buffer = String::new();
} else if !in_double_quote {
in_single_quote = true;
} else {
buffer.push_str(grapheme);
}
escaped = false;
}
" " => {
if in_double_quote || in_single_quote || escaped {
buffer.push_str(grapheme);
} else {
if !buffer.is_empty() {
result.push(buffer);
}
buffer = String::new();
}
escaped = false;
}
_ => {
buffer.push_str(grapheme);
escaped = false;
}
}
}
if !buffer.is_empty() {
result.push(buffer);
}
result
.iter()
.skip(1)
.filter(|s| !s.starts_with('-'))
.map(std::borrow::ToOwned::to_owned)
.collect()
}
#[cfg(test)]
mod tests {
use super::{normalize_path, parse_mv_command};
use std::env;
use std::path::PathBuf;
#[test]
#[cfg(not(windows))]
fn normalize_path_works_absolute_paths() {
assert_eq!(normalize_path("/foo/bar/baz"), String::from("/foo/bar/baz"));
assert_eq!(normalize_path("/"), String::from("/"));
assert_eq!(normalize_path("////"), String::from("/"));
}
#[test]
#[cfg(not(windows))]
fn normalize_path_works_with_tilda() {
assert_eq!(normalize_path("~/"), env::var("HOME").unwrap());
assert_eq!(
normalize_path("~/foo"),
PathBuf::from(env::var("HOME").unwrap())
.join("foo")
.to_string_lossy()
);
}
#[test]
#[cfg(not(windows))]
fn normalize_path_works_with_double_dots() {
assert_eq!(normalize_path("/foo/bar/../baz"), String::from("/foo/baz"));
assert_eq!(normalize_path("/foo/bar/../../baz"), String::from("/baz"));
assert_eq!(normalize_path("/foo/bar/../../"), String::from("/"));
assert_eq!(normalize_path("/foo/bar/../.."), String::from("/"));
assert_eq!(
normalize_path("~/foo/bar/../baz"),
PathBuf::from(env::var("HOME").unwrap())
.join("foo/baz")
.to_string_lossy()
);
assert_eq!(normalize_path("~/foo/bar/../.."), env::var("HOME").unwrap());
}
#[cfg(windows)]
fn windows_home_path() -> String {
PathBuf::from(env::var("HOMEDRIVE").unwrap())
.join(env::var("HOMEPATH").unwrap())
.to_str()
.unwrap()
.to_string()
}
#[test]
#[cfg(windows)]
fn normalize_path_works_absolute_paths() {
assert_eq!(
normalize_path("C:\\foo\\bar\\baz"),
String::from("C:\\foo\\bar\\baz")
);
assert_eq!(normalize_path("C:\\"), String::from("C:\\"));
assert_eq!(normalize_path("C:\\\\\\\\"), String::from("C:\\"));
}
#[test]
#[cfg(windows)]
fn normalize_path_works_with_tilda() {
assert_eq!(normalize_path("~\\"), windows_home_path());
assert_eq!(
normalize_path("~\\foo"),
PathBuf::from(windows_home_path())
.join("foo")
.to_string_lossy()
);
}
#[test]
#[cfg(windows)]
fn normalize_path_works_with_double_dots() {
assert_eq!(
normalize_path("C:\\foo\\bar\\..\\baz"),
String::from("C:\\foo\\baz")
);
assert_eq!(
normalize_path("C:\\foo\\bar\\..\\..\\baz"),
String::from("C:\\baz")
);
assert_eq!(
normalize_path("C:\\foo\\bar\\..\\..\\"),
String::from("C:\\")
);
assert_eq!(normalize_path("C:\\foo\\bar\\..\\.."), String::from("C:\\"));
assert_eq!(
normalize_path("~\\foo\\bar\\..\\baz"),
PathBuf::from(windows_home_path())
.join("foo\\baz")
.to_string_lossy()
);
assert_eq!(normalize_path("~\\foo\\bar\\..\\.."), windows_home_path());
}
#[test]
fn parse_mv_command_works_in_the_basic_case() {
assert_eq!(
parse_mv_command("mv foo bar"),
vec!["foo".to_string(), "bar".to_string()]
);
}
#[test]
fn parse_mv_command_works_with_options() {
assert_eq!(
parse_mv_command("mv -v foo bar"),
vec!["foo".to_string(), "bar".to_string()]
);
}
#[test]
fn parse_mv_command_works_with_escaped_strings() {
assert_eq!(
parse_mv_command("mv \"foo baz\" 'bar bing'"),
vec!["foo baz".to_string(), "bar bing".to_string()]
);
assert_eq!(
parse_mv_command("mv -v \"foo\" 'bar'"),
vec!["foo".to_string(), "bar".to_string()]
);
}
#[test]
fn parse_mv_command_works_with_escaping() {
assert_eq!(
parse_mv_command("mv \\foo bar"),
vec!["foo".to_string(), "bar".to_string()]
);
assert_eq!(
parse_mv_command("mv foo\\ bar bing"),
vec!["foo bar".to_string(), "bing".to_string()]
);
assert_eq!(
parse_mv_command("mv \"foo\\ bar\" bing"),
vec!["foo bar".to_string(), "bing".to_string()]
);
assert_eq!(
parse_mv_command("mv \"'foo\\' bar\" bing"),
vec!["'foo' bar".to_string(), "bing".to_string()]
);
assert_eq!(
parse_mv_command("mv \"\\\"foo\" bar"),
vec!["\"foo".to_string(), "bar".to_string()]
);
}
}
================================================
FILE: src/settings.rs
================================================
use crate::cli::{Cli, DumpFormat, SortOrder, SubCommand};
use crate::shell_history;
use crate::time::parse_timestamp;
use clap::Parser;
use config::Source;
use config::Value;
use crossterm::style::Color;
use directories_next::{ProjectDirs, UserDirs};
use regex::Regex;
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
#[derive(Debug)]
pub enum Mode {
Add,
Search,
Train,
Move,
Init,
Dump,
Stats,
}
#[derive(Debug)]
pub enum KeyScheme {
Emacs,
Vim,
}
#[derive(Debug)]
pub enum InitMode {
Bash,
Zsh,
Fish,
Powershell,
}
#[derive(Debug, PartialEq, Eq)]
pub enum InterfaceView {
Top,
Bottom,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResultSort {
Rank,
LastRun,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResultFilter {
Global,
CurrentDirectory,
}
#[derive(Debug, Clone, Copy)]
pub enum HistoryFormat {
/// bash format - commands in plain text, one per line, with multi-line commands joined.
/// HISTTIMEFORMAT is assumed to be empty.
Bash,
/// zsh format - commands in plain text, with multiline commands on multiple lines.
/// `McFly` does not currently handle joining these lines; they're treated as separate commands.
/// If --zsh-extended-history was given, `extended_history` will be true, and we'll strip the
/// timestamp from the beginning of each command.
Zsh { extended_history: bool },
/// fish's pseudo-yaml, with commands stored as 'cmd' with multiple lines joined into one with
/// '\n', and with timestamps stored as 'when'. ('paths' is ignored.)
/// (Some discussion of changing format: https://github.com/fish-shell/fish-shell/pull/6493)
Fish,
}
/// Time range, it can be:
/// - `..`
/// - `since..before`
/// - `since..`
/// - `..before`
#[derive(Debug, Clone, Default)]
pub struct TimeRange {
pub since: Option<i64>,
pub before: Option<i64>,
}
#[derive(Debug)]
pub struct Colors {
pub menubar_bg: Color,
pub menubar_fg: Color,
pub darkmode_colors: DarkModeColors,
pub lightmode_colors: LightModeColors,
}
#[derive(Debug)]
pub struct DarkModeColors {
pub prompt: Color,
pub timing: Color,
pub results_fg: Color,
pub r
gitextract_5xr7nxho/
├── .github/
│ └── workflows/
│ ├── clippy.yml
│ ├── mean_bean_ci.yml
│ ├── mean_bean_deploy.yml
│ └── rustfmt.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.txt
├── Cargo.toml
├── LICENSE
├── README.md
├── ci/
│ ├── before_deploy.sh
│ ├── build.bash
│ ├── common.bash
│ ├── install.sh
│ ├── script.sh
│ ├── set_rust_version.bash
│ └── test.bash
├── dev.bash
├── dev.fish
├── dev.zsh
├── mcfly.bash
├── mcfly.fish
├── mcfly.ps1
├── mcfly.zsh
├── pkg/
│ └── brew/
│ └── mcfly.rb
└── src/
├── cli.rs
├── command_input.rs
├── dumper.rs
├── fake_typer.rs
├── fixed_length_grapheme_string.rs
├── history/
│ ├── db_extensions.rs
│ ├── history.rs
│ ├── mod.rs
│ └── schema.rs
├── history_cleaner.rs
├── init.rs
├── interface.rs
├── lib.rs
├── main.rs
├── network.rs
├── node.rs
├── path_update_helpers.rs
├── settings.rs
├── shell_history.rs
├── simplified_command.rs
├── stats_generator.rs
├── time.rs
├── trainer.rs
├── training_cache.rs
└── training_sample_generator.rs
SYMBOL INDEX (223 symbols across 24 files)
FILE: pkg/brew/mcfly.rb
class Mcfly (line 9) | class Mcfly < Formula
method install (line 23) | def install
method caveats (line 27) | def caveats
FILE: src/cli.rs
type Cli (line 8) | pub struct Cli {
method is_init (line 227) | pub fn is_init(&self) -> bool {
type SubCommand (line 35) | pub enum SubCommand {
type HistoryFormat (line 192) | pub enum HistoryFormat {
type InitMode (line 201) | pub enum InitMode {
type SortOrder (line 210) | pub enum SortOrder {
method to_str (line 235) | pub fn to_str(&self) -> &'static str {
type DumpFormat (line 219) | pub enum DumpFormat {
FILE: src/command_input.rs
type InputCommand (line 5) | pub enum InputCommand {
type Move (line 13) | pub enum Move {
type CommandInput (line 25) | pub struct CommandInput {
method fmt (line 35) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
method from (line 41) | pub fn from<S: Into<String>>(s: S) -> CommandInput {
method clear (line 52) | pub fn clear(&mut self) {
method set (line 57) | pub fn set(&mut self, str: &str) {
method move_cursor (line 62) | pub fn move_cursor(&mut self, direction: Move) {
method delete (line 83) | pub fn delete(&mut self, cmd: Move) {
method insert (line 190) | pub fn insert(&mut self, c: char) {
method recompute_caches (line 211) | fn recompute_caches(&mut self) {
method previous_word_boundary (line 217) | fn previous_word_boundary(&self) -> usize {
method next_word_boundary (line 266) | fn next_word_boundary(&self) -> usize {
function display_works (line 310) | fn display_works() {
function next_word_boundary_works (line 316) | fn next_word_boundary_works() {
function previous_word_boundary_works (line 344) | fn previous_word_boundary_works() {
FILE: src/dumper.rs
type Dumper (line 9) | pub struct Dumper<'a> {
function new (line 16) | pub fn new(settings: &'a Settings, history: &'a History) -> Self {
function dump (line 20) | pub fn dump(&self) {
function dump2json (line 42) | fn dump2json(commands: &[DumpCommand]) -> io::Result<()> {
function dump2csv (line 48) | fn dump2csv(commands: &[DumpCommand]) -> io::Result<()> {
FILE: src/fake_typer.rs
function ioctl (line 7) | pub fn ioctl(fd: libc::c_int, request: libc::c_ulong, arg: ...) -> libc:...
function use_tiocsti (line 12) | pub fn use_tiocsti(string: &str) {
function use_tiocsti (line 23) | pub fn use_tiocsti(string: &str) {
FILE: src/fixed_length_grapheme_string.rs
type FixedLengthGraphemeString (line 5) | pub struct FixedLengthGraphemeString {
method empty (line 25) | pub fn empty(max_grapheme_length: u16) -> FixedLengthGraphemeString {
method new (line 33) | pub fn new<S: Into<String>>(s: S, max_grapheme_length: u16) -> FixedLe...
method push_grapheme_str (line 40) | pub fn push_grapheme_str<S: Into<String>>(&mut self, s: S) {
method push_str (line 50) | pub fn push_str(&mut self, s: &str) {
method write (line 12) | fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
method flush (line 18) | fn flush(&mut self) -> std::io::Result<()> {
function length_works (line 60) | fn length_works() {
function max_length_works (line 66) | fn max_length_works() {
FILE: src/history/db_extensions.rs
function add_db_functions (line 6) | pub fn add_db_functions(db: &Connection) {
FILE: src/history/history.rs
type Features (line 22) | pub struct Features {
type Command (line 36) | pub struct Command {
method fmt (line 59) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
type DumpCommand (line 52) | pub struct DumpCommand {
method from (line 65) | fn from(command: Command) -> Self {
function ser_to_datetime (line 71) | fn ser_to_datetime<S>(when_run: &i64, serializer: S) -> Result<S::Ok, S:...
type History (line 79) | pub struct History {
method load (line 96) | pub fn load(history_format: HistoryFormat) -> History {
method should_add (line 107) | pub fn should_add(&self, command: &str) -> bool {
method add (line 137) | pub fn add(
method determine_if_selected_from_ui (line 162) | fn determine_if_selected_from_ui(&self, command: &str, session_id: &st...
method record_selected_from_ui (line 193) | pub fn record_selected_from_ui(&self, command: &str, session_id: &str,...
method possibly_update_paths (line 203) | pub fn possibly_update_paths(&self, command: &str, exit_code: Option<i...
method find_matches (line 250) | pub fn find_matches(
method is_case_sensitive (line 436) | fn is_case_sensitive(cmd: &str) -> bool {
method calc_match_indices (line 441) | fn calc_match_indices(text: &str, cmd: &str, fuzzy: i16) -> Vec<usize> {
method build_cache_table (line 474) | pub fn build_cache_table(
method commands (line 645) | pub fn commands(
method run_query (line 692) | pub fn run_query<T, F>(&self, query: &str, params: &[(&str, &dyn ToSql...
method last_command (line 710) | pub fn last_command(&self, session_id: &Option<String>) -> Option<Comm...
method last_command_templates (line 714) | pub fn last_command_templates(
method delete_command (line 726) | pub fn delete_command(&self, command: &str) {
method update_paths (line 744) | pub fn update_paths(&self, old_path: &str, new_path: &str, print_outpu...
method dump (line 787) | pub fn dump(&self, time_range: &TimeRange, order: &SortOrder) -> Vec<D...
method from_shell_history (line 825) | fn from_shell_history(history_format: HistoryFormat) -> History {
method from_db_path (line 918) | fn from_db_path(path: PathBuf) -> History {
constant IGNORED_COMMANDS (line 84) | const IGNORED_COMMANDS: [&str; 7] = [
FILE: src/history/schema.rs
constant CURRENT_SCHEMA_VERSION (line 6) | pub const CURRENT_SCHEMA_VERSION: u16 = 3;
function first_time_setup (line 8) | pub fn first_time_setup(connection: &Connection) {
function migrate (line 13) | pub fn migrate(connection: &Connection) {
function make_schema_versions_table (line 91) | fn make_schema_versions_table(connection: &Connection) {
function write_current_schema_version (line 106) | fn write_current_schema_version(connection: &Connection) {
function cmd_strings (line 115) | fn cmd_strings(connection: &Connection) -> Vec<(i64, String)> {
FILE: src/history_cleaner.rs
function clean (line 8) | pub fn clean(settings: &Settings, history: &History, command: &str) {
function clean_temporary_files (line 38) | fn clean_temporary_files(mcfly_history: &Path, history_format: HistoryFo...
FILE: src/init.rs
type Init (line 4) | pub struct Init {}
method new (line 7) | pub fn new(init_mode: &InitMode) -> Self {
method init_bash (line 24) | pub fn init_bash() {
method init_zsh (line 28) | pub fn init_zsh() {
method init_fish (line 32) | pub fn init_fish() {
method init_pwsh (line 36) | pub fn init_pwsh() {
FILE: src/interface.rs
type Interface (line 20) | pub struct Interface<'a> {
type SelectionResult (line 35) | pub struct SelectionResult {
type MoveSelection (line 44) | pub enum MoveSelection {
type MenuMode (line 50) | pub enum MenuMode {
method text (line 56) | fn text(&self, interface: &Interface) -> String {
method bg (line 95) | fn bg(&self, normal: Color) -> Color {
constant PROMPT_LINE_INDEX (line 103) | const PROMPT_LINE_INDEX: u16 = 3;
constant INFO_LINE_INDEX (line 104) | const INFO_LINE_INDEX: u16 = 1;
constant RESULTS_TOP_INDEX (line 105) | const RESULTS_TOP_INDEX: u16 = 5;
function new (line 108) | pub fn new(settings: &'a Settings, history: &'a History) -> Interface<'a> {
function display (line 125) | pub fn display(&mut self) -> SelectionResult {
function build_cache_table (line 152) | fn build_cache_table(&self) {
function menubar (line 164) | fn menubar<W: Write>(&self, screen: &mut W) {
function prompt (line 187) | fn prompt<W: Write>(&self, screen: &mut W) {
function debug_cursor (line 206) | fn debug_cursor<W: Write>(&self, screen: &mut W) {
function results (line 216) | fn results<W: Write>(&mut self, screen: &mut W) {
function debug (line 353) | fn debug<W: Write, S: Into<String>>(&self, screen: &mut W, s: S) {
function move_selection (line 365) | fn move_selection(&mut self, direction: MoveSelection) {
function accept_selection (line 391) | fn accept_selection(&mut self) {
function confirm (line 397) | fn confirm(&mut self, confirmation: bool) {
function delete_selection (line 404) | fn delete_selection(&mut self) {
function refresh_matches (line 416) | fn refresh_matches(&mut self, reset_selection: bool) {
function switch_result_sort (line 428) | fn switch_result_sort(&mut self) {
function switch_result_filter (line 435) | fn switch_result_filter(&mut self) {
function select (line 443) | fn select(&mut self) {
function select_with_emacs_key_scheme (line 523) | fn select_with_emacs_key_scheme(&mut self, event: Event) -> bool {
function handle_emacs_keyevent (line 537) | fn handle_emacs_keyevent(&mut self, event: KeyEvent) -> bool {
function select_with_vim_key_scheme (line 719) | fn select_with_vim_key_scheme(&mut self, event: KeyEvent) -> bool {
function truncate_for_display (line 983) | fn truncate_for_display(
function result_top_index (line 1050) | fn result_top_index(&self) -> u16 {
function prompt_line_index (line 1059) | fn prompt_line_index(&self) -> u16 {
function info_line_index (line 1067) | fn info_line_index(&self) -> u16 {
function command_line_index (line 1075) | fn command_line_index(&self, index: i16) -> i16 {
function is_screen_view_bottom (line 1082) | fn is_screen_view_bottom(&self) -> bool {
FILE: src/main.rs
function handle_addition (line 16) | fn handle_addition(settings: &Settings) {
function handle_search (line 45) | fn handle_search(settings: &Settings) {
function handle_train (line 86) | fn handle_train(settings: &Settings) {
function handle_move (line 91) | fn handle_move(settings: &Settings) {
function handle_init (line 96) | fn handle_init(settings: &Settings) {
function handle_dump (line 100) | fn handle_dump(settings: &Settings) {
function handle_stats (line 105) | fn handle_stats(settings: &Settings) {
function main (line 111) | fn main() {
FILE: src/network.rs
type Network (line 8) | pub struct Network {
method random (line 74) | pub fn random() -> Network {
method compute (line 92) | pub fn compute(&mut self, features: &Features) {
method dot (line 103) | pub fn dot(&self, features: &Features) -> f64 {
method output (line 113) | pub fn output(&self, features: &Features) -> f64 {
method average_error (line 118) | pub fn average_error(&self, generator: &TrainingSampleGenerator, recor...
method default (line 19) | fn default() -> Network {
FILE: src/node.rs
type Node (line 6) | pub struct Node {
method random (line 22) | pub fn random() -> Node {
method dot (line 41) | pub fn dot(&self, features: &Features) -> f64 {
method output (line 56) | pub fn output(&self, features: &Features) -> f64 {
function test_dot (line 66) | fn test_dot() {
FILE: src/path_update_helpers.rs
function normalize_path (line 7) | pub fn normalize_path(incoming_path: &str) -> String {
function parse_mv_command (line 18) | pub fn parse_mv_command(command: &str) -> Vec<String> {
function normalize_path_works_absolute_paths (line 98) | fn normalize_path_works_absolute_paths() {
function normalize_path_works_with_tilda (line 106) | fn normalize_path_works_with_tilda() {
function normalize_path_works_with_double_dots (line 118) | fn normalize_path_works_with_double_dots() {
function windows_home_path (line 133) | fn windows_home_path() -> String {
function normalize_path_works_absolute_paths (line 143) | fn normalize_path_works_absolute_paths() {
function normalize_path_works_with_tilda (line 154) | fn normalize_path_works_with_tilda() {
function normalize_path_works_with_double_dots (line 166) | fn normalize_path_works_with_double_dots() {
function parse_mv_command_works_in_the_basic_case (line 190) | fn parse_mv_command_works_in_the_basic_case() {
function parse_mv_command_works_with_options (line 198) | fn parse_mv_command_works_with_options() {
function parse_mv_command_works_with_escaped_strings (line 206) | fn parse_mv_command_works_with_escaped_strings() {
function parse_mv_command_works_with_escaping (line 218) | fn parse_mv_command_works_with_escaping() {
FILE: src/settings.rs
type Mode (line 18) | pub enum Mode {
type KeyScheme (line 29) | pub enum KeyScheme {
type InitMode (line 35) | pub enum InitMode {
type InterfaceView (line 43) | pub enum InterfaceView {
type ResultSort (line 49) | pub enum ResultSort {
type ResultFilter (line 55) | pub enum ResultFilter {
type HistoryFormat (line 61) | pub enum HistoryFormat {
type TimeRange (line 84) | pub struct TimeRange {
method is_full (line 743) | pub fn is_full(&self) -> bool {
type Colors (line 90) | pub struct Colors {
type DarkModeColors (line 98) | pub struct DarkModeColors {
type LightModeColors (line 110) | pub struct LightModeColors {
type Settings (line 122) | pub struct Settings {
method parse_args (line 230) | pub fn parse_args() -> Settings {
method load_config (line 498) | pub fn load_config(&mut self) {
method merge_config (line 508) | pub fn merge_config(&mut self, config_map: HashMap<String, Value>) {
method mcfly_training_cache_path (line 661) | pub fn mcfly_training_cache_path() -> PathBuf {
method mcfly_db_path (line 669) | pub fn mcfly_db_path() -> PathBuf {
method mcfly_config_path (line 681) | pub fn mcfly_config_path() -> PathBuf {
method mcfly_xdg_dir (line 687) | fn mcfly_xdg_dir() -> ProjectDirs {
method mcfly_base_path (line 691) | fn mcfly_base_path(base_dir: PathBuf) -> PathBuf {
method mcfly_dir_in_home (line 695) | fn mcfly_dir_in_home() -> Option<PathBuf> {
method default (line 163) | fn default() -> Settings {
function pwd (line 707) | pub fn pwd() -> String {
function pwd (line 713) | pub fn pwd() -> String {
function is_env_var_truthy (line 725) | fn is_env_var_truthy(name: &str) -> bool {
FILE: src/shell_history.rs
function read_ignoring_utf_errors (line 15) | fn read_ignoring_utf_errors(path: &Path) -> String {
function read_and_unmetafy (line 25) | fn read_and_unmetafy(path: &Path) -> String {
function has_leading_timestamp (line 41) | fn has_leading_timestamp(line: &str) -> bool {
function history_file_path (line 58) | pub fn history_file_path() -> PathBuf {
type HistoryCommand (line 75) | pub struct HistoryCommand {
method new (line 85) | pub fn new<S>(command: S, when: i64, format: HistoryFormat) -> Self
method fmt (line 98) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
function full_history (line 114) | pub fn full_history(path: &Path, history_format: HistoryFormat) -> Vec<H...
function last_history_line (line 176) | pub fn last_history_line(path: &Path, history_format: HistoryFormat) -> ...
function delete_last_history_entry_if_search (line 183) | pub fn delete_last_history_entry_if_search(
function delete_lines (line 225) | pub fn delete_lines(path: &Path, history_format: HistoryFormat, command:...
function append_history_entry (line 242) | pub fn append_history_entry(command: &HistoryCommand, path: &Path, debug...
function has_leading_timestamp_works (line 266) | fn has_leading_timestamp_works() {
FILE: src/simplified_command.rs
constant TRUNCATE_TO_N_TOKENS (line 3) | const TRUNCATE_TO_N_TOKENS: u16 = 2;
type SimplifiedCommand (line 6) | pub struct SimplifiedCommand {
method new (line 19) | pub fn new<S: Into<String>>(command: S, truncate: bool) -> SimplifiedC...
method simplify (line 29) | fn simplify(&mut self) {
function it_works_for_simple_commands (line 99) | fn it_works_for_simple_commands() {
function it_simplifies_simple_quoted_strings (line 111) | fn it_simplifies_simple_quoted_strings() {
function it_handles_one_level_of_quote_escaping (line 126) | fn it_handles_one_level_of_quote_escaping() {
function it_ignores_escaping_otherwise (line 133) | fn it_ignores_escaping_otherwise() {
function it_simplifies_obvious_paths (line 139) | fn it_simplifies_obvious_paths() {
function it_ignores_leading_paths (line 170) | fn it_ignores_leading_paths() {
function it_truncates_after_simplification (line 182) | fn it_truncates_after_simplification() {
FILE: src/stats_generator.rs
type StatsGenerator (line 10) | pub struct StatsGenerator<'a> {
type StatItem (line 15) | struct StatItem {
function generate_stats (line 23) | pub fn generate_stats(&self, settings: &Settings) -> String {
function generate_command_stats (line 61) | fn generate_command_stats(&self, cmds: i16, stats: Vec<StatItem>) -> Str...
function most_used_commands (line 99) | fn most_used_commands(
function new (line 216) | pub fn new(history: &'a History) -> Self {
function count_commands_from_db_history (line 219) | fn count_commands_from_db_history(&self, dir: &Option<String>) -> i32 {
FILE: src/time.rs
function parse_timestamp (line 4) | pub fn parse_timestamp(s: &str) -> i64 {
function to_datetime (line 13) | pub fn to_datetime(timestamp: i64) -> String {
FILE: src/trainer.rs
type Trainer (line 9) | pub struct Trainer<'a> {
function new (line 15) | pub fn new(settings: &'a Settings, history: &'a mut History) -> Trainer<...
function train (line 19) | pub fn train(&mut self) {
FILE: src/training_cache.rs
function write (line 7) | pub fn write(data_set: &[(Features, bool)], cache_path: &Path) {
function read (line 18) | pub fn read(cache_path: &Path) -> Vec<(Features, bool)> {
function output_header (line 48) | fn output_header(writer: &mut Writer<File>) {
function output_row (line 69) | fn output_row(writer: &mut Writer<File>, features: &Features, correct: b...
FILE: src/training_sample_generator.rs
type TrainingSampleGenerator (line 10) | pub struct TrainingSampleGenerator {
method new (line 15) | pub fn new(settings: &Settings, history: &History) -> TrainingSampleGe...
method generate_data_set (line 33) | pub fn generate_data_set(history: &History) -> Vec<(Features, bool)> {
method generate (line 100) | pub fn generate<F>(&self, records: Option<usize>, mut handler: F)
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (281K chars).
[
{
"path": ".github/workflows/clippy.yml",
"chars": 408,
"preview": "name: clippy\non: [push, pull_request]\njobs:\n clippy:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checko"
},
{
"path": ".github/workflows/mean_bean_ci.yml",
"chars": 2861,
"preview": "name: Mean Bean CI\n\non: [push, pull_request]\n\njobs:\n install-cross:\n runs-on: ubuntu-latest\n steps:\n - uses:"
},
{
"path": ".github/workflows/mean_bean_deploy.yml",
"chars": 5791,
"preview": "on:\n push:\n # # Sequence of patterns matched against refs/tags\n tags:\n - \"v*\" # Push events to matching v*, "
},
{
"path": ".github/workflows/rustfmt.yml",
"chars": 460,
"preview": "# When pushed to master, run `cargo +nightly fmt --all` and open a PR.\nname: rustfmt\non: [push, pull_request]\njobs:\n ru"
},
{
"path": ".gitignore",
"chars": 75,
"preview": ".idea\n/target\n**/*.rs.bk\nupdate.sh\n.zsh_history\n.zshrc\n/.fish\n.vscode/**/*\n"
},
{
"path": ".travis.yml",
"chars": 3763,
"preview": "# Based on the \"trust\" template v0.1.2\n# https://github.com/japaric/trust/tree/v0.1.2\n\ndist: trusty\nlanguage: rust\nservi"
},
{
"path": "CHANGELOG.txt",
"chars": 7963,
"preview": "0.9.4 - Dec 24, 2025\n - Implement case sensitivity for inputs containing uppercase letters, highlighing only matched cha"
},
{
"path": "Cargo.toml",
"chars": 1477,
"preview": "[package]\nname = \"mcfly\"\nversion = \"0.9.4\"\nauthors = [\"Andrew Cantino <cantino@users.noreply.github.com>\"]\nedition = \"20"
},
{
"path": "LICENSE",
"chars": 1076,
"preview": "The MIT License\n\nCopyright (c) 2018, Andrew Cantino\n\nPermission is hereby granted, free of charge, to any person obtaini"
},
{
"path": "README.md",
"chars": 15339,
"preview": "> **Seeking co-maintainers**:\n> I don't have much time to maintain this project these days. If someone would like to jum"
},
{
"path": "ci/before_deploy.sh",
"chars": 693,
"preview": "# This script takes care of building your crate and packaging it for release\n\nset -ex\n\nmain() {\n local src=$(pwd) \\\n "
},
{
"path": "ci/build.bash",
"chars": 572,
"preview": "#!/usr/bin/env bash\n# Script for building your rust projects.\nset -e\n\nsource ci/common.bash\n\n# $1 {path} = Path to cross"
},
{
"path": "ci/common.bash",
"chars": 110,
"preview": "required_arg() {\n if [ -z \"$1\" ]; then\n echo \"Required argument $2 missing\"\n exit 1\n fi\n}\n"
},
{
"path": "ci/install.sh",
"chars": 3594,
"preview": "#!/bin/sh\n\n# Heavily modified from https://github.com/japaric/trust/blob/gh-pages/install.sh.\n\nhelp() {\n cat <<'EOF'\n"
},
{
"path": "ci/script.sh",
"chars": 446,
"preview": "# This script takes care of testing your crate\n\nset -ex\n\nmain() {\n cross build --target $TARGET\n cross build --tar"
},
{
"path": "ci/set_rust_version.bash",
"chars": 66,
"preview": "#!/usr/bin/env bash\nset -e\nrustup default $1\nrustup target add $2\n"
},
{
"path": "ci/test.bash",
"chars": 359,
"preview": "#!/usr/bin/env bash\n# Script for building your rust projects.\nset -e\n\nsource ci/common.bash\n\n# $1 {path} = Path to cross"
},
{
"path": "dev.bash",
"chars": 467,
"preview": "#!/bin/bash\n# Build mcfly and run a dev environment bash for local mcfly testing\n\nif ! this_dir=$(cd \"$(dirname \"$0\")\" &"
},
{
"path": "dev.fish",
"chars": 532,
"preview": "#!/bin/bash\n# Build mcfly and run a dev environment fish for local mcfly testing\n\nthis_dir=$(cd `dirname \"$0\"`; pwd)\n\n# "
},
{
"path": "dev.zsh",
"chars": 607,
"preview": "#!/bin/bash\n# Build mcfly and run a dev environment zsh for local mcfly testing\n\nthis_dir=$(cd `dirname \"$0\"`; pwd)\n\n# S"
},
{
"path": "mcfly.bash",
"chars": 7557,
"preview": "#!/bin/bash\n\nfunction mcfly_initialize {\n # Note: We avoid using [[ ... ]] to check the Bash version because we are\n #"
},
{
"path": "mcfly.fish",
"chars": 3527,
"preview": "#!/usr/bin/env fish\n\n# Avoid loading this file more than once\nif test \"$__MCFLY_LOADED\" != \"loaded\"\n set -g __MCFLY_LOA"
},
{
"path": "mcfly.ps1",
"chars": 4232,
"preview": "#!/usr/bin/env pwsh\n\n$null = New-Module mcfly {\n # We need PSReadLine for a number of capabilities\n if ($null -eq "
},
{
"path": "mcfly.zsh",
"chars": 3851,
"preview": "#!/bin/zsh\n\n() {\n # Ensure an interactive shell\n [[ -o interactive ]] || return 0\n\n # Setup MCFLY_HISTFILE and make s"
},
{
"path": "pkg/brew/mcfly.rb",
"chars": 1062,
"preview": "# To install:\n# brew tap cantino/mcfly\n# brew install mcfly\n#\n# To remove:\n# brew uninstall mcfly\n# brew untap c"
},
{
"path": "src/cli.rs",
"chars": 6247,
"preview": "use clap::{Parser, Subcommand, ValueEnum};\nuse regex::Regex;\nuse std::path::PathBuf;\n\n/// Fly through your shell history"
},
{
"path": "src/command_input.rs",
"chars": 10801,
"preview": "use std::fmt;\nuse unicode_segmentation::UnicodeSegmentation;\n\n#[derive(Debug)]\npub enum InputCommand {\n Insert(char),"
},
{
"path": "src/dumper.rs",
"chars": 1660,
"preview": "use std::io::{self, BufWriter, Write};\n\nuse crate::cli::DumpFormat;\nuse crate::history::{DumpCommand, History};\nuse crat"
},
{
"path": "src/fake_typer.rs",
"chars": 673,
"preview": "#[cfg(not(windows))]\nuse libc;\n\n// Should we be using https://docs.rs/libc/0.2.44/libc/fn.ioctl.html instead?\n#[cfg(not("
},
{
"path": "src/fixed_length_grapheme_string.rs",
"chars": 2089,
"preview": "use std::io::Write;\nuse unicode_segmentation::UnicodeSegmentation;\n\n#[derive(Debug)]\npub struct FixedLengthGraphemeStrin"
},
{
"path": "src/history/db_extensions.rs",
"chars": 1486,
"preview": "use crate::history::history::Features;\nuse crate::network::Network;\nuse rusqlite::Connection;\nuse rusqlite::functions::F"
},
{
"path": "src/history/history.rs",
"chars": 38353,
"preview": "#![allow(clippy::module_inception)]\nuse crate::cli::SortOrder;\nuse crate::history::{db_extensions, schema};\nuse crate::n"
},
{
"path": "src/history/mod.rs",
"chars": 111,
"preview": "pub use self::history::{Command, DumpCommand, Features, History};\n\nmod db_extensions;\nmod history;\nmod schema;\n"
},
{
"path": "src/history/schema.rs",
"chars": 4734,
"preview": "use crate::simplified_command::SimplifiedCommand;\nuse rusqlite::{Connection, named_params};\nuse std::io;\nuse std::io::Wr"
},
{
"path": "src/history_cleaner.rs",
"chars": 2264,
"preview": "use crate::history::History;\nuse crate::settings::{HistoryFormat, Settings};\nuse crate::shell_history;\nuse std::env;\nuse"
},
{
"path": "src/init.rs",
"chars": 1035,
"preview": "use super::settings::InitMode;\nuse std::env;\n\npub struct Init {}\n\nimpl Init {\n pub fn new(init_mode: &InitMode) -> Se"
},
{
"path": "src/interface.rs",
"chars": 36446,
"preview": "use crate::command_input::{CommandInput, Move};\nuse crate::history::History;\n\nuse crate::fixed_length_grapheme_string::F"
},
{
"path": "src/lib.rs",
"chars": 429,
"preview": "pub mod cli;\npub mod command_input;\npub mod dumper;\npub mod fake_typer;\npub mod fixed_length_grapheme_string;\npub mod hi"
},
{
"path": "src/main.rs",
"chars": 4323,
"preview": "use std::fs;\nuse std::path::PathBuf;\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nuse mcfly::dumper::Dumper;\nuse mcfly::fak"
},
{
"path": "src/network.rs",
"chars": 4719,
"preview": "#![allow(clippy::unreadable_literal)]\nuse crate::history::Features;\nuse crate::node::Node;\nuse crate::training_sample_ge"
},
{
"path": "src/node.rs",
"chars": 2766,
"preview": "use crate::history::Features;\nuse rand::Rng;\nuse std::f64;\n\n#[derive(Debug, Copy, Clone, Default)]\npub struct Node {\n "
},
{
"path": "src/path_update_helpers.rs",
"chars": 7362,
"preview": "use crate::settings::pwd;\nuse path_absolutize::Absolutize;\nuse std::path::Path;\nuse unicode_segmentation::UnicodeSegment"
},
{
"path": "src/settings.rs",
"chars": 25303,
"preview": "use crate::cli::{Cli, DumpFormat, SortOrder, SubCommand};\nuse crate::shell_history;\nuse crate::time::parse_timestamp;\nus"
},
{
"path": "src/shell_history.rs",
"chars": 9712,
"preview": "use crate::settings::HistoryFormat;\nuse regex::Regex;\nuse std::env;\nuse std::fmt;\nuse std::fs;\nuse std::fs::File;\nuse st"
},
{
"path": "src/simplified_command.rs",
"chars": 7866,
"preview": "use unicode_segmentation::UnicodeSegmentation;\n\nconst TRUNCATE_TO_N_TOKENS: u16 = 2;\n\n#[derive(Debug)]\npub struct Simpli"
},
{
"path": "src/stats_generator.rs",
"chars": 7424,
"preview": "use std::cmp::min;\nuse std::collections::HashMap;\n\nuse serde::Serialize;\n\nuse crate::history::History;\nuse crate::settin"
},
{
"path": "src/time.rs",
"chars": 472,
"preview": "use chrono::{DateTime, Local, TimeZone};\n\n#[must_use]\npub fn parse_timestamp(s: &str) -> i64 {\n chrono_systemd_time::"
},
{
"path": "src/trainer.rs",
"chars": 17446,
"preview": "use crate::history::Features;\nuse crate::history::History;\nuse crate::network::Network;\nuse crate::node::Node;\nuse crate"
},
{
"path": "src/training_cache.rs",
"chars": 3287,
"preview": "use crate::history::Features;\nuse csv::Reader;\nuse csv::Writer;\nuse std::fs::File;\nuse std::path::Path;\n\npub fn write(da"
},
{
"path": "src/training_sample_generator.rs",
"chars": 4377,
"preview": "use crate::history::Command;\nuse crate::history::Features;\nuse crate::history::History;\nuse crate::settings::{ResultFilt"
}
]
About this extraction
This page contains the full source code of the cantino/mcfly GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (262.0 KB), approximately 64.1k tokens, and a symbol index with 223 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.