Feature: zoxide edit subcommand
Allows editing on the backing db by opening up $EDITOR with a human-editable format. Upon exit of the editor, changes are validated and written back to the DB. Fixes: #453
This commit is contained in:
parent
9d9bcfcac2
commit
635a97e812
|
|
@ -181,6 +181,20 @@ dependencies = [
|
||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"terminal_size",
|
||||||
|
"unicode-width",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
|
|
@ -191,6 +205,17 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dialoguer"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"tempfile",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "difflib"
|
name = "difflib"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -235,6 +260,12 @@ version = "1.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
|
checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
|
@ -674,6 +705,16 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal_size"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termtree"
|
name = "termtree"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|
@ -730,6 +771,12 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
|
@ -814,6 +861,12 @@ dependencies = [
|
||||||
"shell-words",
|
"shell-words",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zoxide"
|
name = "zoxide"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
@ -825,6 +878,7 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"clap_complete_fig",
|
"clap_complete_fig",
|
||||||
|
"dialoguer",
|
||||||
"dirs",
|
"dirs",
|
||||||
"dunce",
|
"dunce",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,13 @@ anyhow = "1.0.32"
|
||||||
askama = { version = "0.11.0", default-features = false }
|
askama = { version = "0.11.0", default-features = false }
|
||||||
bincode = "1.3.1"
|
bincode = "1.3.1"
|
||||||
clap = { version = "3.1.0", features = ["derive"] }
|
clap = { version = "3.1.0", features = ["derive"] }
|
||||||
|
dialoguer = "0.10.2"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
dunce = "1.0.1"
|
dunce = "1.0.1"
|
||||||
fastrand = "1.7.0"
|
fastrand = "1.7.0"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
serde = { version = "1.0.116", features = ["derive"] }
|
serde = { version = "1.0.116", features = ["derive"] }
|
||||||
|
tempfile = "3.1.0"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
nix = { version = "0.24.1", default-features = false, features = [
|
nix = { version = "0.24.1", default-features = false, features = [
|
||||||
|
|
@ -47,7 +49,6 @@ clap_complete_fig = "3.1.0"
|
||||||
assert_cmd = "2.0.0"
|
assert_cmd = "2.0.0"
|
||||||
rstest = { version = "0.15.0", default-features = false }
|
rstest = { version = "0.15.0", default-features = false }
|
||||||
rstest_reuse = "0.4.0"
|
rstest_reuse = "0.4.0"
|
||||||
tempfile = "3.1.0"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,14 @@ _arguments "${_arguments_options[@]}" \
|
||||||
'*::paths:_files -/' \
|
'*::paths:_files -/' \
|
||||||
&& ret=0
|
&& ret=0
|
||||||
;;
|
;;
|
||||||
|
(edit)
|
||||||
|
_arguments "${_arguments_options[@]}" \
|
||||||
|
'-h[Print help information]' \
|
||||||
|
'--help[Print help information]' \
|
||||||
|
'-V[Print version information]' \
|
||||||
|
'--version[Print version information]' \
|
||||||
|
&& ret=0
|
||||||
|
;;
|
||||||
(import)
|
(import)
|
||||||
_arguments "${_arguments_options[@]}" \
|
_arguments "${_arguments_options[@]}" \
|
||||||
'--from=[Application to import from]:FROM:(autojump z)' \
|
'--from=[Application to import from]:FROM:(autojump z)' \
|
||||||
|
|
@ -97,6 +105,7 @@ esac
|
||||||
_zoxide_commands() {
|
_zoxide_commands() {
|
||||||
local commands; commands=(
|
local commands; commands=(
|
||||||
'add:Add a new directory or increment its rank' \
|
'add:Add a new directory or increment its rank' \
|
||||||
|
'edit:Modify list of paths and rankings in default editor' \
|
||||||
'import:Import entries from another application' \
|
'import:Import entries from another application' \
|
||||||
'init:Generate shell configuration' \
|
'init:Generate shell configuration' \
|
||||||
'query:Search for a directory in the database' \
|
'query:Search for a directory in the database' \
|
||||||
|
|
@ -109,6 +118,11 @@ _zoxide__add_commands() {
|
||||||
local commands; commands=()
|
local commands; commands=()
|
||||||
_describe -t commands 'zoxide add commands' commands "$@"
|
_describe -t commands 'zoxide add commands' commands "$@"
|
||||||
}
|
}
|
||||||
|
(( $+functions[_zoxide__edit_commands] )) ||
|
||||||
|
_zoxide__edit_commands() {
|
||||||
|
local commands; commands=()
|
||||||
|
_describe -t commands 'zoxide edit commands' commands "$@"
|
||||||
|
}
|
||||||
(( $+functions[_zoxide__import_commands] )) ||
|
(( $+functions[_zoxide__import_commands] )) ||
|
||||||
_zoxide__import_commands() {
|
_zoxide__import_commands() {
|
||||||
local commands; commands=()
|
local commands; commands=()
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
|
||||||
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
|
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
|
||||||
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
|
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
|
||||||
[CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank')
|
[CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank')
|
||||||
|
[CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Modify list of paths and rankings in default editor')
|
||||||
[CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application')
|
[CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application')
|
||||||
[CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration')
|
[CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration')
|
||||||
[CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database')
|
[CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database')
|
||||||
|
|
@ -39,6 +40,13 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
|
||||||
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
|
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
'zoxide;edit' {
|
||||||
|
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
|
||||||
|
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
|
||||||
|
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
|
||||||
|
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
|
||||||
|
break
|
||||||
|
}
|
||||||
'zoxide;import' {
|
'zoxide;import' {
|
||||||
[CompletionResult]::new('--from', 'from', [CompletionResultType]::ParameterName, 'Application to import from')
|
[CompletionResult]::new('--from', 'from', [CompletionResultType]::ParameterName, 'Application to import from')
|
||||||
[CompletionResult]::new('--merge', 'merge', [CompletionResultType]::ParameterName, 'Merge into existing database')
|
[CompletionResult]::new('--merge', 'merge', [CompletionResultType]::ParameterName, 'Merge into existing database')
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ _zoxide() {
|
||||||
add)
|
add)
|
||||||
cmd+="__add"
|
cmd+="__add"
|
||||||
;;
|
;;
|
||||||
|
edit)
|
||||||
|
cmd+="__edit"
|
||||||
|
;;
|
||||||
import)
|
import)
|
||||||
cmd+="__import"
|
cmd+="__import"
|
||||||
;;
|
;;
|
||||||
|
|
@ -34,7 +37,7 @@ _zoxide() {
|
||||||
|
|
||||||
case "${cmd}" in
|
case "${cmd}" in
|
||||||
zoxide)
|
zoxide)
|
||||||
opts="-h -V --help --version add import init query remove"
|
opts="-h -V --help --version add edit import init query remove"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -61,6 +64,20 @@ _zoxide() {
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
zoxide__edit)
|
||||||
|
opts="-h -V --help --version"
|
||||||
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
case "${prev}" in
|
||||||
|
*)
|
||||||
|
COMPREPLY=()
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
zoxide__import)
|
zoxide__import)
|
||||||
opts="-h -V --from --merge --help --version <PATH>"
|
opts="-h -V --from --merge --help --version <PATH>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ set edit:completion:arg-completer[zoxide] = {|@words|
|
||||||
cand -V 'Print version information'
|
cand -V 'Print version information'
|
||||||
cand --version 'Print version information'
|
cand --version 'Print version information'
|
||||||
cand add 'Add a new directory or increment its rank'
|
cand add 'Add a new directory or increment its rank'
|
||||||
|
cand edit 'Modify list of paths and rankings in default editor'
|
||||||
cand import 'Import entries from another application'
|
cand import 'Import entries from another application'
|
||||||
cand init 'Generate shell configuration'
|
cand init 'Generate shell configuration'
|
||||||
cand query 'Search for a directory in the database'
|
cand query 'Search for a directory in the database'
|
||||||
|
|
@ -34,6 +35,12 @@ set edit:completion:arg-completer[zoxide] = {|@words|
|
||||||
cand -V 'Print version information'
|
cand -V 'Print version information'
|
||||||
cand --version 'Print version information'
|
cand --version 'Print version information'
|
||||||
}
|
}
|
||||||
|
&'zoxide;edit'= {
|
||||||
|
cand -h 'Print help information'
|
||||||
|
cand --help 'Print help information'
|
||||||
|
cand -V 'Print version information'
|
||||||
|
cand --version 'Print version information'
|
||||||
|
}
|
||||||
&'zoxide;import'= {
|
&'zoxide;import'= {
|
||||||
cand --from 'Application to import from'
|
cand --from 'Application to import from'
|
||||||
cand --merge 'Merge into existing database'
|
cand --merge 'Merge into existing database'
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
complete -c zoxide -n "__fish_use_subcommand" -s h -l help -d 'Print help information'
|
complete -c zoxide -n "__fish_use_subcommand" -s h -l help -d 'Print help information'
|
||||||
complete -c zoxide -n "__fish_use_subcommand" -s V -l version -d 'Print version information'
|
complete -c zoxide -n "__fish_use_subcommand" -s V -l version -d 'Print version information'
|
||||||
complete -c zoxide -n "__fish_use_subcommand" -f -a "add" -d 'Add a new directory or increment its rank'
|
complete -c zoxide -n "__fish_use_subcommand" -f -a "add" -d 'Add a new directory or increment its rank'
|
||||||
|
complete -c zoxide -n "__fish_use_subcommand" -f -a "edit" -d 'Modify list of paths and rankings in default editor'
|
||||||
complete -c zoxide -n "__fish_use_subcommand" -f -a "import" -d 'Import entries from another application'
|
complete -c zoxide -n "__fish_use_subcommand" -f -a "import" -d 'Import entries from another application'
|
||||||
complete -c zoxide -n "__fish_use_subcommand" -f -a "init" -d 'Generate shell configuration'
|
complete -c zoxide -n "__fish_use_subcommand" -f -a "init" -d 'Generate shell configuration'
|
||||||
complete -c zoxide -n "__fish_use_subcommand" -f -a "query" -d 'Search for a directory in the database'
|
complete -c zoxide -n "__fish_use_subcommand" -f -a "query" -d 'Search for a directory in the database'
|
||||||
complete -c zoxide -n "__fish_use_subcommand" -f -a "remove" -d 'Remove a directory from the database'
|
complete -c zoxide -n "__fish_use_subcommand" -f -a "remove" -d 'Remove a directory from the database'
|
||||||
complete -c zoxide -n "__fish_seen_subcommand_from add" -s h -l help -d 'Print help information'
|
complete -c zoxide -n "__fish_seen_subcommand_from add" -s h -l help -d 'Print help information'
|
||||||
complete -c zoxide -n "__fish_seen_subcommand_from add" -s V -l version -d 'Print version information'
|
complete -c zoxide -n "__fish_seen_subcommand_from add" -s V -l version -d 'Print version information'
|
||||||
|
complete -c zoxide -n "__fish_seen_subcommand_from edit" -s h -l help -d 'Print help information'
|
||||||
|
complete -c zoxide -n "__fish_seen_subcommand_from edit" -s V -l version -d 'Print version information'
|
||||||
complete -c zoxide -n "__fish_seen_subcommand_from import" -l from -d 'Application to import from' -r -f -a "{autojump ,z }"
|
complete -c zoxide -n "__fish_seen_subcommand_from import" -l from -d 'Application to import from' -r -f -a "{autojump ,z }"
|
||||||
complete -c zoxide -n "__fish_seen_subcommand_from import" -l merge -d 'Merge into existing database'
|
complete -c zoxide -n "__fish_seen_subcommand_from import" -l merge -d 'Merge into existing database'
|
||||||
complete -c zoxide -n "__fish_seen_subcommand_from import" -s h -l help -d 'Print help information'
|
complete -c zoxide -n "__fish_seen_subcommand_from import" -s h -l help -d 'Print help information'
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,20 @@ const completion: Fig.Spec = {
|
||||||
template: "folders",
|
template: "folders",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "edit",
|
||||||
|
description: "Modify list of paths and rankings in default editor",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: ["-h", "--help"],
|
||||||
|
description: "Print help information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ["-V", "--version"],
|
||||||
|
description: "Print version information",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "import",
|
name: "import",
|
||||||
description: "Import entries from another application",
|
description: "Import entries from another application",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const ENV_HELP: &str = "ENVIRONMENT VARIABLES:
|
||||||
)]
|
)]
|
||||||
pub enum Cmd {
|
pub enum Cmd {
|
||||||
Add(Add),
|
Add(Add),
|
||||||
|
Edit(Edit),
|
||||||
Import(Import),
|
Import(Import),
|
||||||
Init(Init),
|
Init(Init),
|
||||||
Query(Query),
|
Query(Query),
|
||||||
|
|
@ -37,6 +38,10 @@ pub struct Add {
|
||||||
pub paths: Vec<PathBuf>,
|
pub paths: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Modify list of paths and rankings in default editor
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct Edit {}
|
||||||
|
|
||||||
/// Import entries from another application
|
/// Import entries from another application
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct Import {
|
pub struct Import {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
use crate::cmd::{Edit, Run};
|
||||||
|
use crate::db::{db_path, Database, DatabaseFile, Epoch, Rank};
|
||||||
|
use crate::util::{rename, resolve_path};
|
||||||
|
use crate::{config, util};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::fmt::Write as FmtWrite;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use core::mem;
|
||||||
|
use dialoguer::{Editor, Input};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
const HEADER: &str = "\
|
||||||
|
# Blank lines and lines prepended with '#' are ignored; Line order is insignificant
|
||||||
|
# last_accessed,rank,path
|
||||||
|
";
|
||||||
|
|
||||||
|
enum ValidationResult {
|
||||||
|
Success,
|
||||||
|
Retry,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Run for Edit {
|
||||||
|
fn run(&self) -> Result<()> {
|
||||||
|
let temp_dir = tempdir()?;
|
||||||
|
let temp_dir_path = temp_dir.path();
|
||||||
|
while let Some(db_edits) = get_db_edits()? {
|
||||||
|
let mut db_file = DatabaseFile::new(temp_dir_path);
|
||||||
|
let mut db = db_file.open()?;
|
||||||
|
let result = validate_db(&mut db, db_edits);
|
||||||
|
match result {
|
||||||
|
ValidationResult::Success => {
|
||||||
|
db.save()?;
|
||||||
|
mem::drop(db);
|
||||||
|
mem::drop(db_file);
|
||||||
|
rename(db_path(temp_dir_path), db_path(config::data_dir()?))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
ValidationResult::Exit => break,
|
||||||
|
ValidationResult::Retry => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Zoxide database not altered");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_db_edits() -> Result<Option<String>> {
|
||||||
|
let data_dir = config::data_dir()?;
|
||||||
|
let mut db = DatabaseFile::new(data_dir);
|
||||||
|
let mut db = db.open()?;
|
||||||
|
let mut stream = db.stream(util::current_time().unwrap());
|
||||||
|
let mut to_edit = String::from(HEADER);
|
||||||
|
while let Some(dir) = stream.next() {
|
||||||
|
writeln!(&mut to_edit, "{},{},{}", dir.last_accessed, dir.rank, dir.path)?;
|
||||||
|
}
|
||||||
|
Ok(Editor::new().edit(&to_edit)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_db(db: &mut Database, db_edits: String) -> ValidationResult {
|
||||||
|
let lines = db_edits.lines();
|
||||||
|
|
||||||
|
let mut errors: Vec<(usize, String)> = Vec::new();
|
||||||
|
let mut warnings: Vec<(usize, String)> = Vec::new();
|
||||||
|
|
||||||
|
for (index, line) in lines.enumerate() {
|
||||||
|
let line_number = index + 1;
|
||||||
|
let first_char = line.trim().chars().next();
|
||||||
|
if let Some(first_char) = first_char {
|
||||||
|
if first_char == '#' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut split = line.split(',');
|
||||||
|
let (last_accessed_txt, rank_txt, path_txt) = (split.next(), split.next(), split.next());
|
||||||
|
if split.next().is_some() {
|
||||||
|
errors.push((line_number, "Too many values on line".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_accessed: Option<Epoch> = match last_accessed_txt {
|
||||||
|
Some(value) => match value.trim().parse::<Epoch>() {
|
||||||
|
Ok(value) => Some(value),
|
||||||
|
Err(e) => {
|
||||||
|
errors.push((line_number, e.to_string()));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
errors.push((line_number, "Cannot parse 'last_accessed' field".to_string()));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let rank: Option<Rank> = match rank_txt {
|
||||||
|
Some(value) => match value.trim().parse::<Rank>() {
|
||||||
|
Ok(value) => Some(value),
|
||||||
|
Err(e) => {
|
||||||
|
errors.push((line_number, e.to_string()));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
errors.push((line_number, "Cannot parse 'rank' field".to_string()));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path: Option<String> = match path_txt {
|
||||||
|
Some(value) => {
|
||||||
|
if value.trim() != value {
|
||||||
|
warnings.push((line_number, "path contains trailing whitespace".to_string()));
|
||||||
|
}
|
||||||
|
match resolve_path(&PathBuf::from(value)) {
|
||||||
|
Ok(v) => {
|
||||||
|
if v.to_str().unwrap() != value {
|
||||||
|
errors.push((line_number, "path must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
Some(value.to_string())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
errors.push((line_number, e.to_string()));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
errors.push((line_number, "Cannot parse 'path' field".to_string()));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let (Some(path), Some(last_accessed), Some(rank)) = (path, last_accessed, rank) {
|
||||||
|
db.add_raw(&path, last_accessed, rank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let has_warnings = !warnings.is_empty();
|
||||||
|
let has_errors = !errors.is_empty();
|
||||||
|
if has_warnings {
|
||||||
|
println!("Warnings:");
|
||||||
|
for (line_num, warning) in warnings {
|
||||||
|
println!("{line_num}: {warning}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
if has_errors {
|
||||||
|
println!("Errors:");
|
||||||
|
for (line_num, error) in errors {
|
||||||
|
println!("line {line_num}: {error}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
if has_warnings || has_errors {
|
||||||
|
println!("You may:");
|
||||||
|
println!("(e)dit the file again");
|
||||||
|
println!("e(x)it without saving changes");
|
||||||
|
if !has_errors {
|
||||||
|
println!("(s)ave changes and exit (DANGER!)");
|
||||||
|
}
|
||||||
|
let selection = Input::new()
|
||||||
|
.with_prompt("Choice")
|
||||||
|
.validate_with(|input: &String| -> Result<(), &str> {
|
||||||
|
if input == "e" || input == "x" || (input == "s" && !has_errors) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Invalid selection.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.interact()
|
||||||
|
.unwrap();
|
||||||
|
return match selection.as_str() {
|
||||||
|
"e" => ValidationResult::Retry,
|
||||||
|
"s" => ValidationResult::Success,
|
||||||
|
"x" => ValidationResult::Exit,
|
||||||
|
i => panic!("Expected 'e', 's', or 'x'. Received {i}"), // We already validated input above
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ValidationResult::Success
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod add;
|
mod add;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
|
mod edit;
|
||||||
mod import;
|
mod import;
|
||||||
mod init;
|
mod init;
|
||||||
mod query;
|
mod query;
|
||||||
|
|
@ -17,6 +18,7 @@ impl Run for Cmd {
|
||||||
fn run(&self) -> Result<()> {
|
fn run(&self) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Cmd::Add(cmd) => cmd.run(),
|
Cmd::Add(cmd) => cmd.run(),
|
||||||
|
Cmd::Edit(cmd) => cmd.run(),
|
||||||
Cmd::Import(cmd) => cmd.run(),
|
Cmd::Import(cmd) => cmd.run(),
|
||||||
Cmd::Init(cmd) => cmd.run(),
|
Cmd::Init(cmd) => cmd.run(),
|
||||||
Cmd::Query(cmd) => cmd.run(),
|
Cmd::Query(cmd) => cmd.run(),
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,19 @@ impl<'file> Database<'file> {
|
||||||
|
|
||||||
/// Adds a new directory or increments its rank. Also updates its last accessed time.
|
/// Adds a new directory or increments its rank. Also updates its last accessed time.
|
||||||
pub fn add<S: AsRef<str>>(&mut self, path: S, now: Epoch) {
|
pub fn add<S: AsRef<str>>(&mut self, path: S, now: Epoch) {
|
||||||
|
self.add_raw(path, now, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_raw<S: AsRef<str>>(&mut self, path: S, last_accessed: Epoch, rank: Rank) {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
match self.dirs.iter_mut().find(|dir| dir.path == path) {
|
match self.dirs.iter_mut().find(|dir| dir.path == path) {
|
||||||
None => {
|
None => {
|
||||||
self.dirs.push(Dir { path: path.to_string().into(), last_accessed: now, rank: 1.0 });
|
self.dirs.push(Dir { path: path.to_string().into(), last_accessed, rank });
|
||||||
}
|
}
|
||||||
Some(dir) => {
|
Some(dir) => {
|
||||||
dir.last_accessed = now;
|
dir.last_accessed = last_accessed;
|
||||||
dir.rank += 1.0;
|
dir.rank += rank;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -140,7 +144,7 @@ impl DatabaseFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn db_path<P: AsRef<Path>>(data_dir: P) -> PathBuf {
|
pub fn db_path<P: AsRef<Path>>(data_dir: P) -> PathBuf {
|
||||||
const DB_FILENAME: &str = "db.zo";
|
const DB_FILENAME: &str = "db.zo";
|
||||||
data_dir.as_ref().join(DB_FILENAME)
|
data_dir.as_ref().join(DB_FILENAME)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ fn tmpfile<P: AsRef<Path>>(dir: P) -> Result<(File, PathBuf)> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Similar to [`fs::rename`], but retries on Windows.
|
/// Similar to [`fs::rename`], but retries on Windows.
|
||||||
fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
|
pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
|
||||||
const MAX_ATTEMPTS: usize = 5;
|
const MAX_ATTEMPTS: usize = 5;
|
||||||
let from = from.as_ref();
|
let from = from.as_ref();
|
||||||
let to = to.as_ref();
|
let to = to.as_ref();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue