diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b38fe2..34e0ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - POSIX: support for non-Cygwin Windows environments (e.g. Busybox). +- `import` now supports fetching entries from `atuin`. + +### Changed + +- `import` now auto-detects database files. ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 27da609..45eb6f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "difflib" version = "0.4.0" @@ -436,6 +445,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "once_cell" version = "1.21.3" @@ -484,6 +499,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -712,18 +733,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -804,6 +835,36 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1004,5 +1065,6 @@ dependencies = [ "rstest_reuse", "serde", "tempfile", + "time", "which", ] diff --git a/Cargo.toml b/Cargo.toml index d137115..b801097 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ fastrand = "2.0.0" glob = "0.3.0" ouroboros = "0.18.3" serde = { version = "1.0.116", features = ["derive"] } +time = { version = "0.3.47", default-features = false, features = ["parsing", "macros", "std"] } [target.'cfg(unix)'.dependencies] nix = { version = "0.30.1", default-features = false, features = [ diff --git a/README.md b/README.md index 05981d5..878fba0 100644 --- a/README.md +++ b/README.md @@ -350,61 +350,21 @@ zoxide can be installed in 4 easy steps: 4. **Import your data** (optional) If you currently use any of these plugins, you may want to import your data - into zoxide: + into zoxide. The data file is auto-detected using each plugin's standard + conventions. -
- autojump + ```sh + zoxide import + ``` - > Run this command in your terminal: - > - > ```sh - > zoxide import --from=autojump "/path/to/autojump/db" - > ``` - > - > The path usually varies according to your system: - > - > | OS | Path | Example | - > | ------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------ | - > | Linux | `$XDG_DATA_HOME/autojump/autojump.txt` or `$HOME/.local/share/autojump/autojump.txt` | `/home/alice/.local/share/autojump/autojump.txt` | - > | macOS | `$HOME/Library/autojump/autojump.txt` | `/Users/Alice/Library/autojump/autojump.txt` | - > | Windows | `%APPDATA%\autojump\autojump.txt` | `C:\Users\Alice\AppData\Roaming\autojump\autojump.txt` | - -
- -
- fasd, z, z.lua, zsh-z - - > Run this command in your terminal: - > - > ```sh - > zoxide import --from=z "path/to/z/db" - > ``` - > - > The path usually varies according to your system: - > - > | Plugin | Path | - > | ---------------- | ----------------------------------------------------------------------------------- | - > | fasd | `$_FASD_DATA` or `$HOME/.fasd` | - > | z (bash/zsh) | `$_Z_DATA` or `$HOME/.z` | - > | z (fish) | `$Z_DATA` or `$XDG_DATA_HOME/z/data` or `$HOME/.local/share/z/data` | - > | z.lua (bash/zsh) | `$_ZL_DATA` or `$HOME/.zlua` | - > | z.lua (fish) | `$XDG_DATA_HOME/zlua/zlua.txt` or `$HOME/.local/share/zlua/zlua.txt` or `$_ZL_DATA` | - > | zsh-z | `$ZSHZ_DATA` or `$_Z_DATA` or `$HOME/.z` | - -
- -
- ZLocation - - > Run this command in PowerShell: - > - > ```powershell - > $db = New-TemporaryFile - > (Get-ZLocation).GetEnumerator() | ForEach-Object { Write-Output ($_.Name+'|'+$_.Value+'|0') } | Out-File $db - > zoxide import --from=z $db - > ``` - -
+ | Plugin | Command | + | ---------- | ------------------------- | + | atuin | `zoxide import atuin` | + | autojump | `zoxide import autojump` | + | fasd | `zoxide import fasd` | + | z | `zoxide import z` | + | z.lua | `zoxide import z.lua` | + | zsh-z | `zoxide import zsh-z` | ## Configuration diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 97e654f..96c571a 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -96,14 +96,78 @@ esac ;; (import) _arguments "${_arguments_options[@]}" : \ -'--from=[Application to import from]:FROM:(autojump z)' \ '--merge[Merge into existing database]' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ '--version[Print version]' \ -':path:_files' \ +":: :_zoxide__import_commands" \ +"*::: :->import" \ && ret=0 + + case $state in + (import) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:zoxide-import-command-$line[1]:" + case $line[1] in + (atuin) +_arguments "${_arguments_options[@]}" : \ +'--merge[Merge into existing database]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 +;; +(autojump) +_arguments "${_arguments_options[@]}" : \ +'--merge[Merge into existing database]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 +;; +(fasd) +_arguments "${_arguments_options[@]}" : \ +'--merge[Merge into existing database]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 +;; +(z) +_arguments "${_arguments_options[@]}" : \ +'--merge[Merge into existing database]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 +;; +(z.lua) +_arguments "${_arguments_options[@]}" : \ +'--merge[Merge into existing database]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 +;; +(zsh-z) +_arguments "${_arguments_options[@]}" : \ +'--merge[Merge into existing database]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 +;; + esac + ;; +esac ;; (init) _arguments "${_arguments_options[@]}" : \ @@ -199,9 +263,46 @@ _zoxide__edit__reload_commands() { } (( $+functions[_zoxide__import_commands] )) || _zoxide__import_commands() { - local commands; commands=() + local commands; commands=( +'atuin:Import from atuin' \ +'autojump:Import from autojump' \ +'fasd:Import from fasd' \ +'z:Import from z' \ +'z.lua:Import from z.lua' \ +'zsh-z:Import from zsh-z' \ + ) _describe -t commands 'zoxide import commands' commands "$@" } +(( $+functions[_zoxide__import__atuin_commands] )) || +_zoxide__import__atuin_commands() { + local commands; commands=() + _describe -t commands 'zoxide import atuin commands' commands "$@" +} +(( $+functions[_zoxide__import__autojump_commands] )) || +_zoxide__import__autojump_commands() { + local commands; commands=() + _describe -t commands 'zoxide import autojump commands' commands "$@" +} +(( $+functions[_zoxide__import__fasd_commands] )) || +_zoxide__import__fasd_commands() { + local commands; commands=() + _describe -t commands 'zoxide import fasd commands' commands "$@" +} +(( $+functions[_zoxide__import__z_commands] )) || +_zoxide__import__z_commands() { + local commands; commands=() + _describe -t commands 'zoxide import z commands' commands "$@" +} +(( $+functions[_zoxide__import__z.lua_commands] )) || +_zoxide__import__z.lua_commands() { + local commands; commands=() + _describe -t commands 'zoxide import z.lua commands' commands "$@" +} +(( $+functions[_zoxide__import__zsh-z_commands] )) || +_zoxide__import__zsh-z_commands() { + local commands; commands=() + _describe -t commands 'zoxide import zsh-z commands' commands "$@" +} (( $+functions[_zoxide__init_commands] )) || _zoxide__init_commands() { local commands; commands=() diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index bb47d3a..a76c12c 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -82,7 +82,60 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { break } 'zoxide;import' { - [CompletionResult]::new('--from', '--from', [CompletionResultType]::ParameterName, 'Application to import from') + [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('atuin', 'atuin', [CompletionResultType]::ParameterValue, 'Import from atuin') + [CompletionResult]::new('autojump', 'autojump', [CompletionResultType]::ParameterValue, 'Import from autojump') + [CompletionResult]::new('fasd', 'fasd', [CompletionResultType]::ParameterValue, 'Import from fasd') + [CompletionResult]::new('z', 'z', [CompletionResultType]::ParameterValue, 'Import from z') + [CompletionResult]::new('z.lua', 'z.lua', [CompletionResultType]::ParameterValue, 'Import from z.lua') + [CompletionResult]::new('zsh-z', 'zsh-z', [CompletionResultType]::ParameterValue, 'Import from zsh-z') + break + } + 'zoxide;import;atuin' { + [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;import;autojump' { + [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;import;fasd' { + [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;import;z' { + [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;import;z.lua' { + [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;import;zsh-z' { [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 82b174e..6325bca 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -46,6 +46,24 @@ _zoxide() { zoxide__edit,reload) cmd="zoxide__edit__reload" ;; + zoxide__import,atuin) + cmd="zoxide__import__atuin" + ;; + zoxide__import,autojump) + cmd="zoxide__import__autojump" + ;; + zoxide__import,fasd) + cmd="zoxide__import__fasd" + ;; + zoxide__import,z) + cmd="zoxide__import__z" + ;; + zoxide__import,z.lua) + cmd="zoxide__import__z.lua" + ;; + zoxide__import,zsh-z) + cmd="zoxide__import__zsh__z" + ;; *) ;; esac @@ -159,16 +177,96 @@ _zoxide() { return 0 ;; zoxide__import) - opts="-h -V --from --merge --help --version " + opts="-h -V --merge --help --version atuin autojump fasd z z.lua zsh-z" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in - --from) - COMPREPLY=($(compgen -W "autojump z" -- "${cur}")) - return 0 + *) + COMPREPLY=() ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__import__atuin) + opts="-h -V --merge --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__import__autojump) + opts="-h -V --merge --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__import__fasd) + opts="-h -V --merge --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__import__z) + opts="-h -V --merge --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__import__z.lua) + opts="-h -V --merge --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__import__zsh__z) + opts="-h -V --merge --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in *) COMPREPLY=() ;; diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 93c57af..5ba33fa 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -72,7 +72,54 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand --version 'Print version' } &'zoxide;import'= { - cand --from 'Application to import from' + cand --merge 'Merge into existing database' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + cand atuin 'Import from atuin' + cand autojump 'Import from autojump' + cand fasd 'Import from fasd' + cand z 'Import from z' + cand z.lua 'Import from z.lua' + cand zsh-z 'Import from zsh-z' + } + &'zoxide;import;atuin'= { + cand --merge 'Merge into existing database' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;import;autojump'= { + cand --merge 'Merge into existing database' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;import;fasd'= { + cand --merge 'Merge into existing database' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;import;z'= { + cand --merge 'Merge into existing database' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;import;z.lua'= { + cand --merge 'Merge into existing database' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;import;zsh-z'= { cand --merge 'Merge into existing database' cand -h 'Print help' cand --help 'Print help' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 3a0bfe7..6d7e5b2 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -49,11 +49,33 @@ complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subc complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from increment" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from reload" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from reload" -s V -l version -d 'Print version' -complete -c zoxide -n "__fish_zoxide_using_subcommand import" -l from -d 'Application to import from' -r -f -a "autojump\t'' -z\t''" -complete -c zoxide -n "__fish_zoxide_using_subcommand import" -l merge -d 'Merge into existing database' -complete -c zoxide -n "__fish_zoxide_using_subcommand import" -s h -l help -d 'Print help' -complete -c zoxide -n "__fish_zoxide_using_subcommand import" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -l merge -d 'Merge into existing database' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -f -a "atuin" -d 'Import from atuin' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -f -a "autojump" -d 'Import from autojump' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -f -a "fasd" -d 'Import from fasd' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -f -a "z" -d 'Import from z' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -f -a "z.lua" -d 'Import from z.lua' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and not __fish_seen_subcommand_from atuin autojump fasd z z.lua zsh-z" -f -a "zsh-z" -d 'Import from zsh-z' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from atuin" -l merge -d 'Merge into existing database' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from atuin" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from atuin" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from autojump" -l merge -d 'Merge into existing database' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from autojump" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from autojump" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from fasd" -l merge -d 'Merge into existing database' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from fasd" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from fasd" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from z" -l merge -d 'Merge into existing database' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from z" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from z" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from z.lua" -l merge -d 'Merge into existing database' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from z.lua" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from z.lua" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from zsh-z" -l merge -d 'Merge into existing database' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from zsh-z" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_zoxide_using_subcommand import; and __fish_seen_subcommand_from zsh-z" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand init" -l cmd -d 'Changes the prefix of the `z` and `zi` commands' -r complete -c zoxide -n "__fish_zoxide_using_subcommand init" -l hook -d 'Changes how often zoxide increments a directory\'s score' -r -f -a "none\t'' prompt\t'' diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index 642908e..70f4d60 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -43,14 +43,50 @@ module completions { --version(-V) # Print version ] - def "nu-complete zoxide import from" [] { - [ "autojump" "z" ] - } - # Import entries from another application export extern "zoxide import" [ - path: path - --from: string@"nu-complete zoxide import from" # Application to import from + --merge # Merge into existing database + --help(-h) # Print help + --version(-V) # Print version + ] + + # Import from atuin + export extern "zoxide import atuin" [ + --merge # Merge into existing database + --help(-h) # Print help + --version(-V) # Print version + ] + + # Import from autojump + export extern "zoxide import autojump" [ + --merge # Merge into existing database + --help(-h) # Print help + --version(-V) # Print version + ] + + # Import from fasd + export extern "zoxide import fasd" [ + --merge # Merge into existing database + --help(-h) # Print help + --version(-V) # Print version + ] + + # Import from z + export extern "zoxide import z" [ + --merge # Merge into existing database + --help(-h) # Print help + --version(-V) # Print version + ] + + # Import from z.lua + export extern "zoxide import z.lua" [ + --merge # Merge into existing database + --help(-h) # Print help + --version(-V) # Print version + ] + + # Import from zsh-z + export extern "zoxide import zsh-z" [ --merge # Merge into existing database --help(-h) # Print help --version(-V) # Print version diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 1e0d404..207da2f 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -114,19 +114,117 @@ const completion: Fig.Spec = { { name: "import", description: "Import entries from another application", - options: [ + subcommands: [ { - name: "--from", - description: "Application to import from", - isRepeatable: true, - args: { - name: "from", - suggestions: [ - "autojump", - "z", - ], - }, + name: "atuin", + description: "Import from atuin", + options: [ + { + name: "--merge", + description: "Merge into existing database", + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], }, + { + name: "autojump", + description: "Import from autojump", + options: [ + { + name: "--merge", + description: "Merge into existing database", + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, + { + name: "fasd", + description: "Import from fasd", + options: [ + { + name: "--merge", + description: "Merge into existing database", + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, + { + name: "z", + description: "Import from z", + options: [ + { + name: "--merge", + description: "Merge into existing database", + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, + { + name: "z.lua", + description: "Import from z.lua", + options: [ + { + name: "--merge", + description: "Merge into existing database", + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, + { + name: "zsh-z", + description: "Import from zsh-z", + options: [ + { + name: "--merge", + description: "Merge into existing database", + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, + ], + options: [ { name: "--merge", description: "Merge into existing database", @@ -140,10 +238,6 @@ const completion: Fig.Spec = { description: "Print version", }, ], - args: { - name: "path", - template: "filepaths", - }, }, { name: "init", diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 7359786..0de2ee5 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -95,23 +95,30 @@ pub enum EditCommand { help_template = HelpTemplate, )] pub struct Import { - #[clap(value_hint = ValueHint::FilePath)] - pub path: PathBuf, - - /// Application to import from - #[clap(value_enum, long)] + #[clap(subcommand)] pub from: ImportFrom, /// Merge into existing database - #[clap(long)] + #[clap(long, global = true)] pub merge: bool, } -#[derive(ValueEnum, Clone, Debug)] +#[derive(Subcommand, Clone, Debug)] pub enum ImportFrom { + /// Import from atuin + Atuin, + /// Import from autojump Autojump, - #[clap(alias = "fasd")] + /// Import from fasd + Fasd, + /// Import from z Z, + /// Import from z.lua + #[clap(name = "z.lua")] + ZLua, + /// Import from zsh-z + #[clap(name = "zsh-z")] + ZshZ, } /// Generate shell configuration diff --git a/src/cmd/import.rs b/src/cmd/import.rs index ac0777a..1a53eed 100644 --- a/src/cmd/import.rs +++ b/src/cmd/import.rs @@ -1,166 +1,25 @@ -use std::fs; - -use anyhow::{Context, Result, bail}; +use anyhow::{Result, bail}; use crate::cmd::{Import, ImportFrom, Run}; use crate::db::Database; +use crate::import; impl Run for Import { fn run(&self) -> Result<()> { - let buffer = fs::read_to_string(&self.path).with_context(|| { - format!("could not open database for importing: {}", &self.path.display()) - })?; - let mut db = Database::open()?; if !self.merge && !db.dirs().is_empty() { bail!("current database is not empty, specify --merge to continue anyway"); } match self.from { - ImportFrom::Autojump => import_autojump(&mut db, &buffer), - ImportFrom::Z => import_z(&mut db, &buffer), + ImportFrom::Atuin => import::run(&import::Atuin {}, &mut db)?, + ImportFrom::Autojump => import::run(&import::Autojump {}, &mut db)?, + ImportFrom::Fasd => import::run(&import::Fasd {}, &mut db)?, + ImportFrom::Z => import::run(&import::Z {}, &mut db)?, + ImportFrom::ZLua => import::run(&import::ZLua {}, &mut db)?, + ImportFrom::ZshZ => import::run(&import::ZshZ {}, &mut db)?, } - .context("import error")?; db.save() } } - -fn import_autojump(db: &mut Database, buffer: &str) -> Result<()> { - for line in buffer.lines() { - if line.is_empty() { - continue; - } - let (rank, path) = - line.split_once('\t').with_context(|| format!("invalid entry: {line}"))?; - - let mut rank = rank.parse::().with_context(|| format!("invalid rank: {rank}"))?; - // Normalize the rank using a sigmoid function. Don't import actual ranks from - // autojump, since its scoring algorithm is very different and might - // take a while to normalize. - rank = sigmoid(rank); - - db.add_unchecked(path, rank, 0); - } - - if db.dirty() { - db.dedup(); - } - Ok(()) -} - -fn import_z(db: &mut Database, buffer: &str) -> Result<()> { - for line in buffer.lines() { - if line.is_empty() { - continue; - } - let mut split = line.rsplitn(3, '|'); - - let last_accessed = split.next().with_context(|| format!("invalid entry: {line}"))?; - let last_accessed = - last_accessed.parse().with_context(|| format!("invalid epoch: {last_accessed}"))?; - - let rank = split.next().with_context(|| format!("invalid entry: {line}"))?; - let rank = rank.parse().with_context(|| format!("invalid rank: {rank}"))?; - - let path = split.next().with_context(|| format!("invalid entry: {line}"))?; - - db.add_unchecked(path, rank, last_accessed); - } - - if db.dirty() { - db.dedup(); - } - Ok(()) -} - -fn sigmoid(x: f64) -> f64 { - 1.0 / (1.0 + (-x).exp()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::db::Dir; - - #[test] - fn from_autojump() { - let data_dir = tempfile::tempdir().unwrap(); - let mut db = Database::open_dir(data_dir.path()).unwrap(); - for (path, rank, last_accessed) in [ - ("/quux/quuz", 1.0, 100), - ("/corge/grault/garply", 6.0, 600), - ("/waldo/fred/plugh", 3.0, 300), - ("/xyzzy/thud", 8.0, 800), - ("/foo/bar", 9.0, 900), - ] { - db.add_unchecked(path, rank, last_accessed); - } - - let buffer = "\ -7.0 /baz -2.0 /foo/bar -5.0 /quux/quuz"; - import_autojump(&mut db, buffer).unwrap(); - - db.sort_by_path(); - println!("got: {:?}", &db.dirs()); - - let exp = [ - Dir { path: "/baz".into(), rank: sigmoid(7.0), last_accessed: 0 }, - Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, - Dir { path: "/foo/bar".into(), rank: 9.0 + sigmoid(2.0), last_accessed: 900 }, - Dir { path: "/quux/quuz".into(), rank: 1.0 + sigmoid(5.0), last_accessed: 100 }, - Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, - Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, - ]; - println!("exp: {exp:?}"); - - for (dir1, dir2) in db.dirs().iter().zip(exp) { - assert_eq!(dir1.path, dir2.path); - assert!((dir1.rank - dir2.rank).abs() < 0.01); - assert_eq!(dir1.last_accessed, dir2.last_accessed); - } - } - - #[test] - fn from_z() { - let data_dir = tempfile::tempdir().unwrap(); - let mut db = Database::open_dir(data_dir.path()).unwrap(); - for (path, rank, last_accessed) in [ - ("/quux/quuz", 1.0, 100), - ("/corge/grault/garply", 6.0, 600), - ("/waldo/fred/plugh", 3.0, 300), - ("/xyzzy/thud", 8.0, 800), - ("/foo/bar", 9.0, 900), - ] { - db.add_unchecked(path, rank, last_accessed); - } - - let buffer = "\ -/baz|7|700 -/quux/quuz|4|400 -/foo/bar|2|200 -/quux/quuz|5|500"; - import_z(&mut db, buffer).unwrap(); - - db.sort_by_path(); - println!("got: {:?}", &db.dirs()); - - let exp = [ - Dir { path: "/baz".into(), rank: 7.0, last_accessed: 700 }, - Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, - Dir { path: "/foo/bar".into(), rank: 11.0, last_accessed: 900 }, - Dir { path: "/quux/quuz".into(), rank: 10.0, last_accessed: 500 }, - Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, - Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, - ]; - println!("exp: {exp:?}"); - - for (dir1, dir2) in db.dirs().iter().zip(exp) { - assert_eq!(dir1.path, dir2.path); - assert!((dir1.rank - dir2.rank).abs() < 0.01); - assert_eq!(dir1.last_accessed, dir2.last_accessed); - } - } -} diff --git a/src/import.rs b/src/import.rs new file mode 100644 index 0000000..6af1e5e --- /dev/null +++ b/src/import.rs @@ -0,0 +1,75 @@ +mod atuin; +mod autojump; +mod fasd; +mod z; +mod z_lua; +mod zsh_z; + +pub(crate) use atuin::Atuin; +pub(crate) use autojump::Autojump; +pub(crate) use fasd::Fasd; +pub(crate) use z::Z; +pub(crate) use z_lua::ZLua; +pub(crate) use zsh_z::ZshZ; + +use std::io::{self, Write}; +use std::path::PathBuf; + +use anyhow::Result; + +use crate::config; +use crate::db::{Database, Dir}; + +pub(crate) trait Importer { + /// Yields directory entries to be imported. + /// + /// The outer `Result` reports failure to fetch the input (e.g. missing + /// file, subprocess errored). The per-item `Result` reports a malformed + /// row, which doesn't necessarily abort the whole import. + fn dirs(&self) -> Result, ImportError>>>; +} + +/// A single record that failed to import. +#[derive(Debug)] +pub(crate) struct ImportError { + /// Path of the source file containing the offending record. `None` if the + /// importer is not file-based (e.g. atuin streams from a subprocess). + pub path: Option, + + /// 1-indexed line number of the offending input. + pub line_num: usize, + + /// Underlying reason the record could not be imported. + pub source: anyhow::Error, +} + +/// Drives a single importer end-to-end: writes each `Ok` dir into the +/// database and prints each `Err` to stderr in `:: ` +/// format. Doesn't abort on per-record errors — bad rows are skipped, the +/// rest of the import continues. After the iteration completes successfully, +/// the database is deduplicated and aged. +pub(crate) fn run(importer: &I, db: &mut Database) -> Result<()> { + let stderr = io::stderr(); + let mut stderr = stderr.lock(); + + for entry in importer.dirs()? { + match entry { + Ok(dir) => db.add_unchecked(dir.path, dir.rank, dir.last_accessed), + Err(e) => { + let location = match &e.path { + Some(path) => format!("{}:{}", path.display(), e.line_num), + None => format!("line {}", e.line_num), + }; + let _ = writeln!(stderr, "{location}: {:#}", e.source); + } + } + } + + if db.dirty() { + db.dedup(); + let max_age = config::maxage()?; + db.age(max_age); + } + + Ok(()) +} diff --git a/src/import/atuin.rs b/src/import/atuin.rs new file mode 100644 index 0000000..11b2e82 --- /dev/null +++ b/src/import/atuin.rs @@ -0,0 +1,116 @@ +use std::borrow::Cow; +use std::io::{BufRead, BufReader}; +use std::process::{Child, ChildStdout, Command, Stdio}; +use std::str; + +use anyhow::{Context, Result, anyhow}; + +use crate::db::{Dir, Epoch}; +use crate::import::{ImportError, Importer}; + +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct Atuin {} + +impl Importer for Atuin { + fn dirs(&self) -> Result, ImportError>>> { + // atuin renders `{time}` as `YYYY-MM-DD HH:MM:SS` in UTC. + let mut child = Command::new("atuin") + .args(["history", "list", "--format={time}\t{directory}", "--print0"]) + .stdout(Stdio::piped()) + .spawn() + .context("failed to run `atuin`; is it installed and on PATH?")?; + let stdout = child.stdout.take().expect("stdout piped"); + let reader = BufReader::new(stdout); + Ok(Iter::new(reader, child)) + } +} + +/// Iterates atuin's NUL-separated `{time}\t{directory}` records, emitting one +/// `Dir` per directory transition (consecutive same-path records collapse). +/// Owns the `Child` handle so the subprocess is reaped on Drop. +struct Iter { + reader: BufReader, + buf: Vec, + line_num: usize, + + child: Child, + prev_cwd: Option, +} + +impl Iter { + fn new(reader: BufReader, child: Child) -> Self { + Self { reader, buf: Vec::new(), line_num: 0, child, prev_cwd: None } + } + + fn err(&self, source: anyhow::Error) -> ImportError { + ImportError { path: None, line_num: self.line_num, source } + } + + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + let line = + str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; + + let (timestamp, path) = + line.split_once('\t').ok_or_else(|| self.err(anyhow!("invalid entry: {line}")))?; + + let timestamp_format = + time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); + let timestamp = time::PrimitiveDateTime::parse(timestamp, timestamp_format) + .map_err(|e| self.err(anyhow!(e).context(format!("invalid timestamp: {timestamp:?}"))))? + .assume_utc() + .unix_timestamp(); + + let dir = Dir { + path: Cow::Owned(path.to_string()), + rank: 1.0, + last_accessed: timestamp as Epoch, + }; + Ok(dir) + } +} + +impl Iterator for Iter { + type Item = Result, ImportError>; + + fn next(&mut self) -> Option { + loop { + self.buf.clear(); + self.line_num += 1; + + match self.reader.read_until(b'\0', &mut self.buf) { + Ok(0) => return None, + Ok(_) => { + if self.buf.last() == Some(&b'\0') { + self.buf.pop(); + } + if self.buf.is_empty() { + continue; + } + + let result = self.parse_line(&self.buf); + match &result { + Ok(dir) => { + let path = dir.path.as_ref(); + if self.prev_cwd.as_deref() == Some(path) { + continue; // dedup consecutive same-path entries + } + self.prev_cwd = Some(path.to_string()); + return Some(result); + } + Err(_) => return Some(result), + } + } + Err(e) => { + return Some(Err(self.err(anyhow!(e).context("could not read from atuin")))); + } + } + } + } +} + +impl Drop for Iter { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} diff --git a/src/import/autojump.rs b/src/import/autojump.rs new file mode 100644 index 0000000..15e277a --- /dev/null +++ b/src/import/autojump.rs @@ -0,0 +1,125 @@ +use std::borrow::Cow; +use std::env; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; +use std::str; + +use anyhow::{Context, Result, anyhow}; + +use crate::db::Dir; +use crate::import::{ImportError, Importer}; + +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct Autojump {} + +impl Importer for Autojump { + fn dirs(&self) -> Result, ImportError>>> { + let path = data_path()?; + let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; + let reader = BufReader::new(file); + Ok(Iter::new(reader, path)) + } +} + +struct Iter { + reader: R, + buf: Vec, + line_num: usize, + path: PathBuf, +} + +impl Iter { + fn new(reader: R, path: PathBuf) -> Self { + Self { reader, buf: Vec::new(), line_num: 0, path } + } + + fn err(&self, source: anyhow::Error) -> ImportError { + ImportError { path: Some(self.path.clone()), line_num: self.line_num, source } + } + + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + let line = + str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; + + let (rank, path) = + line.split_once('\t').ok_or_else(|| self.err(anyhow!("invalid entry: {line}")))?; + let rank = rank + .parse::() + .map_err(|e| self.err(anyhow!(e).context(format!("invalid rank: {rank}"))))?; + + // Normalize the rank using a sigmoid function. Don't import actual ranks from + // autojump, since its scoring algorithm is very different and might + // take a while to normalize. + let rank = sigmoid(rank); + + Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed: 0 }) + } +} + +impl Iterator for Iter { + type Item = Result, ImportError>; + + fn next(&mut self) -> Option { + loop { + self.buf.clear(); + self.line_num += 1; + + match self.reader.read_until(b'\n', &mut self.buf) { + Ok(0) => return None, + Ok(_) => { + if self.buf.last() == Some(&b'\n') { + self.buf.pop(); + } + if self.buf.last() == Some(&b'\r') { + self.buf.pop(); + } + if self.buf.is_empty() { + continue; + } + return Some(self.parse_line(&self.buf)); + } + Err(e) => return Some(Err(self.err(anyhow::Error::from(e)))), + } + } + } +} + +/// Mirrors autojump's path logic: +/// +/// ```python +/// if is_osx(): +/// data_home = os.path.join(os.path.expanduser('~'), 'Library') +/// elif is_windows(): +/// data_home = os.getenv('APPDATA') +/// else: +/// data_home = os.getenv( +/// 'XDG_DATA_HOME', +/// os.path.join(os.path.expanduser('~'), '.local', 'share'), +/// ) +/// data_path = os.path.join(data_home, 'autojump', 'autojump.txt') +/// ``` +fn data_path() -> Result { + let mut path = if cfg!(target_os = "macos") { + let mut path = dirs::home_dir().context("could not find home directory")?; + path.push("Library"); + path + } else if cfg!(target_os = "windows") { + let appdata = env::var_os("APPDATA").context("%APPDATA% is not set")?; + PathBuf::from(appdata) + } else if let Some(xdg) = env::var_os("XDG_DATA_HOME") { + PathBuf::from(xdg) + } else { + let mut path = dirs::home_dir().context("could not find home directory")?; + path.push(".local"); + path.push("share"); + path + }; + path.push("autojump"); + path.push("autojump.txt"); + Ok(path) +} + +fn sigmoid(x: f64) -> f64 { + 1.0 / (1.0 + (-x).exp()) +} diff --git a/src/import/fasd.rs b/src/import/fasd.rs new file mode 100644 index 0000000..ec2d060 --- /dev/null +++ b/src/import/fasd.rs @@ -0,0 +1,38 @@ +use std::env; +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; + +use anyhow::{Context, Result}; + +use crate::db::Dir; +use crate::import::{ImportError, Importer, z}; + +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct Fasd {} + +impl Importer for Fasd { + fn dirs(&self) -> Result, ImportError>>> { + let path = data_path()?; + let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; + let reader = BufReader::new(file); + // fasd uses the same `path|rank|last_accessed` line format as z, so reuse z's iterator. + Ok(z::Iter::new(reader, path)) + } +} + +/// Mirrors fasd's path logic: +/// +/// ```sh +/// [ -z "$_FASD_DATA" ] && _FASD_DATA="$HOME/.fasd" +/// ``` +fn data_path() -> Result { + match env::var_os("_FASD_DATA") { + Some(path) => Ok(PathBuf::from(path)), + None => { + let mut path = dirs::home_dir().context("could not find home directory")?; + path.push(".fasd"); + Ok(path) + } + } +} diff --git a/src/import/z.rs b/src/import/z.rs new file mode 100644 index 0000000..1d67708 --- /dev/null +++ b/src/import/z.rs @@ -0,0 +1,104 @@ +use std::borrow::Cow; +use std::env; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; +use std::str; + +use anyhow::{Context, Result, anyhow}; + +use crate::db::Dir; +use crate::import::{ImportError, Importer}; + +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct Z {} + +impl Importer for Z { + fn dirs(&self) -> Result, ImportError>>> { + let path = data_path()?; + let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; + let reader = BufReader::new(file); + Ok(Iter::new(reader, path)) + } +} + +pub(crate) struct Iter { + reader: R, + buf: Vec, + line_num: usize, + path: PathBuf, +} + +impl Iter { + pub(crate) fn new(reader: R, path: PathBuf) -> Self { + Self { reader, buf: Vec::new(), line_num: 0, path } + } + + fn err(&self, source: anyhow::Error) -> ImportError { + ImportError { path: Some(self.path.clone()), line_num: self.line_num, source } + } + + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + let line = + str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; + let err = || self.err(anyhow!("invalid entry: {line}")); + + // z stores entries as `path|rank|last_accessed`. Use `rsplitn` so paths + // containing `|` are preserved. + let mut split = line.rsplitn(3, '|'); + + let last_accessed = split.next().ok_or_else(err)?; + let last_accessed = last_accessed.parse::().map_err(|_| err())?; + + let rank = split.next().ok_or_else(err)?; + let rank = rank.parse::().map_err(|_| err())?; + + let path = split.next().ok_or_else(err)?; + + Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed }) + } +} + +impl Iterator for Iter { + type Item = Result, ImportError>; + + fn next(&mut self) -> Option { + loop { + self.buf.clear(); + self.line_num += 1; + + match self.reader.read_until(b'\n', &mut self.buf) { + Ok(0) => return None, + Ok(_) => { + if self.buf.last() == Some(&b'\n') { + self.buf.pop(); + } + if self.buf.last() == Some(&b'\r') { + self.buf.pop(); + } + if self.buf.is_empty() { + continue; + } + return Some(self.parse_line(&self.buf)); + } + Err(e) => return Some(Err(self.err(anyhow::Error::from(e)))), + } + } + } +} + +/// Mirrors z's path logic: +/// +/// ```sh +/// local datafile="${_Z_DATA:-$HOME/.z}" +/// ``` +fn data_path() -> Result { + match env::var_os("_Z_DATA") { + Some(path) => Ok(PathBuf::from(path)), + None => { + let mut path = dirs::home_dir().context("could not find home directory")?; + path.push(".z"); + Ok(path) + } + } +} diff --git a/src/import/z_lua.rs b/src/import/z_lua.rs new file mode 100644 index 0000000..0ea4584 --- /dev/null +++ b/src/import/z_lua.rs @@ -0,0 +1,108 @@ +use std::env; +use std::ffi::OsStr; +use std::fs::File; +use std::io::{self, BufReader}; +use std::path::PathBuf; + +use anyhow::{Context, Result}; + +use crate::db::Dir; +use crate::import::{ImportError, Importer, z}; + +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct ZLua {} + +impl Importer for ZLua { + fn dirs(&self) -> Result, ImportError>>> { + let path = data_path()?; + let err = match File::open(&path) { + Ok(file) => return Ok(z::Iter::new(BufReader::new(file), path)), + Err(e) if e.kind() == io::ErrorKind::NotFound => e, + Err(e) => return Err(e).with_context(|| format!("could not read {path:?}")), + }; + + let fish_path = data_path_fish()?; + let file = match File::open(&fish_path) { + Ok(file) => file, + // Both paths missing - report the original path's error. + Err(e) if e.kind() == io::ErrorKind::NotFound => { + return Err(err).with_context(|| format!("could not read {path:?}")); + } + // Fish path failed for some other reason (permissions, etc.) + Err(e) => return Err(e).with_context(|| format!("could not read {fish_path:?}")), + }; + // z.lua uses the same `path|rank|last_accessed` line format as z. + Ok(z::Iter::new(BufReader::new(file), fish_path)) + } +} + +/// Mirrors z.lua's path logic: +/// +/// ```lua +/// DATA_FILE = '~/.zlua' -- default +/// +/// -- in z_init(): +/// local _zl_data = os.getenv('_ZL_DATA') +/// if _zl_data ~= nil and _zl_data ~= "" then +/// if windows then +/// DATA_FILE = _zl_data +/// else +/// -- avoid windows environments affect cygwin & msys +/// if not string.match(_zl_data, '^%a:[/\\]') then +/// DATA_FILE = _zl_data +/// end +/// end +/// end +/// ``` +fn data_path() -> Result { + if let Some(path) = env::var_os("_ZL_DATA") + // Skip empty paths. + .filter(|path| !path.is_empty()) + // On non-Windows, skip values that look like a Windows path (`C:\...`) + // — guards against Cygwin/MSYS environments leaking through. + .filter(|path| cfg!(target_os = "windows") || !looks_like_windows_path(path)) + { + return Ok(PathBuf::from(path)); + } + + let mut path = dirs::home_dir().context("could not find home directory")?; + path.push(".zlua"); + + Ok(path) +} + +/// Mirrors z.lua's path logic on Fish: +/// +/// ```fish +/// if test -z "$XDG_DATA_HOME" +/// set -U _ZL_DATA_DIR "$HOME/.local/share/zlua" +/// else +/// set -U _ZL_DATA_DIR "$XDG_DATA_HOME/zlua" +/// end +/// set -x _ZL_DATA "$_ZL_DATA_DIR/zlua.txt" +/// ``` +fn data_path_fish() -> Result { + let mut path = match env::var_os("XDG_DATA_HOME") { + Some(xdg) => PathBuf::from(xdg), + None => { + let mut path = dirs::home_dir().context("could not find home directory")?; + path.push(".local"); + path.push("share"); + path + } + }; + + path.push("zlua"); + path.push("zlua.txt"); + + Ok(path) +} + +/// Matches Lua's `^%a:[/\\]` — ASCII letter, colon, slash-or-backslash. +fn looks_like_windows_path(s: &OsStr) -> bool { + let bytes = s.as_encoded_bytes(); + bytes.len() >= 3 + && bytes[0].is_ascii_alphabetic() + && bytes[1] == b':' + && (bytes[2] == b'/' || bytes[2] == b'\\') +} diff --git a/src/import/zsh_z.rs b/src/import/zsh_z.rs new file mode 100644 index 0000000..652faf8 --- /dev/null +++ b/src/import/zsh_z.rs @@ -0,0 +1,41 @@ +use std::env; +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; + +use anyhow::{Context, Result}; + +use crate::db::Dir; +use crate::import::{ImportError, Importer, z}; + +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct ZshZ {} + +impl Importer for ZshZ { + fn dirs(&self) -> Result, ImportError>>> { + let path = data_path()?; + let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; + let reader = BufReader::new(file); + // zsh-z uses the same `path|rank|last_accessed` line format as z. + Ok(z::Iter::new(reader, path)) + } +} + +/// Mirrors zsh-z's path logic: +/// +/// ```sh +/// # Allow the user to specify a custom datafile in $ZSHZ_DATA (or legacy $_Z_DATA) +/// local custom_datafile="${ZSHZ_DATA:-$_Z_DATA}" +/// # If the user specified a datafile, use that or default to ~/.z +/// local datafile=${${custom_datafile:-$HOME/.z}:A} +/// ``` +fn data_path() -> Result { + match env::var_os("ZSHZ_DATA").or_else(|| env::var_os("_Z_DATA")) { + Some(path) => Ok(PathBuf::from(path)), + None => { + let mut path = dirs::home_dir().context("could not find home directory")?; + path.push(".z"); + Ok(path) + } + } +} diff --git a/src/main.rs b/src/main.rs index d4ddd6e..e432b12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod cmd; mod config; mod db; mod error; +mod import; mod shell; mod util;