From 2dde02cec8599a467ce69105154037afcb3ff168 Mon Sep 17 00:00:00 2001
From: Ajeet D'Souza <98ajeet@gmail.com>
Date: Sun, 10 May 2026 20:02:28 +0530
Subject: [PATCH] import: auto-detect databases; add Atuin support
---
CHANGELOG.md | 5 +
Cargo.lock | 70 +++++++++++++-
Cargo.toml | 1 +
README.md | 66 +++-----------
contrib/completions/_zoxide | 107 +++++++++++++++++++++-
contrib/completions/_zoxide.ps1 | 55 ++++++++++-
contrib/completions/zoxide.bash | 106 ++++++++++++++++++++-
contrib/completions/zoxide.elv | 49 +++++++++-
contrib/completions/zoxide.fish | 32 ++++++-
contrib/completions/zoxide.nu | 48 ++++++++--
contrib/completions/zoxide.ts | 124 ++++++++++++++++++++++---
src/cmd/cmd.rs | 23 +++--
src/cmd/import.rs | 157 ++------------------------------
src/import.rs | 75 +++++++++++++++
src/import/atuin.rs | 116 +++++++++++++++++++++++
src/import/autojump.rs | 125 +++++++++++++++++++++++++
src/import/fasd.rs | 38 ++++++++
src/import/z.rs | 104 +++++++++++++++++++++
src/import/z_lua.rs | 108 ++++++++++++++++++++++
src/import/zsh_z.rs | 41 +++++++++
src/main.rs | 1 +
21 files changed, 1202 insertions(+), 249 deletions(-)
create mode 100644 src/import.rs
create mode 100644 src/import/atuin.rs
create mode 100644 src/import/autojump.rs
create mode 100644 src/import/fasd.rs
create mode 100644 src/import/z.rs
create mode 100644 src/import/z_lua.rs
create mode 100644 src/import/zsh_z.rs
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;