Compare commits

...

11 Commits

Author SHA1 Message Date
zegonix 44d8f2fc43 removed trailing whitespace 2026-05-03 10:09:12 +02:00
scbj 92b1080daa deny bookmark name help 2026-04-16 09:49:27 +02:00
scbj 1512d5117e added completions for book clean 2026-01-28 09:49:17 +01:00
root af40dee02d updated bash setup script 2025-10-25 12:05:50 +02:00
root d66f0defb8 inserted check for bookmarknames 2025-08-04 09:40:03 +02:00
scbj 4e8c311476 improved output of book remove & added todo 2025-07-09 10:39:33 +02:00
scbj 3eee1e304c added todo 2025-07-09 10:31:56 +02:00
scbj b10375dac0 readme fixes 2025-07-08 15:33:54 +02:00
root 71658df5fe implemented options to dedup/rotate stack 2025-07-06 21:21:44 +02:00
scbj 7dcdce624f added todo 2025-07-04 08:48:40 +02:00
root 1005eddeb8 implemented bookmarks subcommand clean to remove entries with invalid
paths
2025-07-03 23:48:24 +02:00
7 changed files with 107 additions and 27 deletions
+18 -11
View File
@@ -10,10 +10,11 @@ I implemented this program to learn rust (because I was put on a rust project at
* `push` - save path to the stack and change to specified directory * `push` - save path to the stack and change to specified directory
* `pop` - pop one, or the specified amount of entries from the stack and move to the oldest one * `pop` - pop one, or the specified amount of entries from the stack and move to the oldest one
* `stack` - display the stack * `stack` - display the stack
* `book` - move to, or add, remove and display bookmarks * `book` - move to/add/remove/display bookmarks
Every shell has its own stack, save in the file `/tmp/navigate/<process-id>`. Every shell has its own stack, saved in the file `/tmp/navigate/<process-id>`.
`navigate` checks for and deletes orphaned stack files on execution. `navigate` checks for and deletes orphaned stack files on execution.
This program does not run background tasks, all state is stored in temporary or configuration files.
## setup ## setup
@@ -21,14 +22,15 @@ Every shell has its own stack, save in the file `/tmp/navigate/<process-id>`.
`navigate` requires a setup `navigate` requires a setup
1) clone the repository 1) clone the repository
1) build crate 1) build crate
1) add path to executable to shell environment 1) add path to executable to shell environment, or copy the executable to a directory in the path variable (e.g. `/usr/local/bin`)
1) source setup script `navigate_bash_setup` 1) source setup script `navigate_bash_setup` for convenience functions and bash completions
## configuration ## configuration
The behaviour of `navigate` can be configured in the file `$XDG_CONFIG_HOME/navigate/navigate.toml`. The behaviour of `navigate` can be configured in the file `$XDG_CONFIG_HOME/navigate/navigate.toml`.
*It has the toml extension, but might not implement the full toml specification.*
> *It has the toml extension, but might not implement the full toml specification.*
> `navigate` will check for the file `default.toml` in the configuration directory and create it if not found. > `navigate` will check for the file `default.toml` in the configuration directory and create it if not found.
> It contains all settings with default values and a short explanation. > It contains all settings with default values and a short explanation.
@@ -36,18 +38,21 @@ The behaviour of `navigate` can be configured in the file `$XDG_CONFIG_HOME/navi
The lines without type and value are categories and need to be defined as toml table (`[table]`) in the configuration file. The lines without type and value are categories and need to be defined as toml table (`[table]`) in the configuration file.
Options are written as `value = key`. Options are written as `value = key`.
Style settings accept styles and one color in the following formats: Style settings accept styles and one color separated by commas.
Make sure to wrap the whole string in single or double quotes.
The following formats are supported:
* **styles**: `bold`, `dim`, `italic`, `underlined`, `blinking`, `reversed`, `invisible`, `strikethrough` * **styles**: `bold`, `dim`, `italic`, `underlined`, `blinking`, `reversed`, `invisible`, `strikethrough`
* **named color**: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` * **named color**: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`
* **numbered color**: `16`..`255` * **numbered color**: `16`..`255`
* **rgb color**: `#rrggbb` * **rgb color**: `#rrggbb`
> *NOTE*: The styles and colors are applied as ansi escape sequences and I do not know a terminal which implements all sequences. > *NOTE*: The styles and colors are applied as ansi escape sequences and your terminal may not support some of them.
# todos # todos
- [x] replace `expect` statements in 'stack.rs' with actual error handling - [ ] replace `std::io::Error` with `thiserror::Error`
- [x] option to pop several entries at a time and option to pop the entire stack - [x] option to pop several entries at a time and option to pop the entire stack
- [x] drop stack - [x] drop stack
- [x] config file - [x] config file
@@ -60,13 +65,15 @@ Style settings accept styles and one color in the following formats:
- [x] `show-bookmarks-on-book` - [x] `show-bookmarks-on-book`
- [x] setting for separator string when displaying stack/bookmarks - [x] setting for separator string when displaying stack/bookmarks
- [x] color option for punctuation (mostly '/') - [x] color option for punctuation (mostly '/')
- [ ] option to dedup stack entries - [x] option to dedup stack entries
- [ ] option for behaviour when jumping to stack entry via `push =<n>` - [x] option for behaviour when jumping to stack entry via `push =<n>`
- [x] bookmarks - [x] bookmarks
- [x] do not resolve links in bookmarks - [x] do not resolve links in bookmarks
- [x] option to show invalid paths - [x] option to show invalid paths
- [x] style option for invalid paths - [x] style option for invalid paths
- [ ] option & subcommand to remove invalid paths - [x] subcommand to remove invalid paths
- [ ] print deleted bookmarks
- [ ] consistent output of `remove` subcommand
- [x] push <number> to push path in stack - [x] push <number> to push path in stack
- [x] write documentation - [x] write documentation
- [x] change config file extension to `.toml` - [x] change config file extension to `.toml`
+11 -5
View File
@@ -1,6 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
__call_navigate() { if ! $(which navigate &>/dev/null); then
return 1
fi
function __call_navigate {
arg_pid=" --pid $$ " arg_pid=" --pid $$ "
eval "$(navigate ${arg_pid} $@)" eval "$(navigate ${arg_pid} $@)"
} }
@@ -29,15 +33,17 @@ function navconfig {
function _book { function _book {
CURRENT_WORD=${COMP_WORDS[COMP_CWORD]} CURRENT_WORD=${COMP_WORDS[COMP_CWORD]}
if [[ COMP_CWORD -eq 1 ]]; then if [[ COMP_CWORD -eq 1 ]]; then
BOOKMARKS="add remove $(__call_navigate bookmark names)" BOOKMARKS="add remove clean $(__call_navigate bookmark completions)"
COMPREPLY=($(compgen -W "${BOOKMARKS}" -- $CURRENT_WORD)) COMPREPLY=($(compgen -W "${BOOKMARKS}" -- $CURRENT_WORD))
elif [[ COMP_CWORD -eq 2 ]]; then elif [[ COMP_CWORD -eq 2 ]]; then
if [[ COMP_WORDS[1] = "remove" ]]; then if [[ "${COMP_WORDS[1]}" = "clean" ]]; then
BOOKMARKS="$(__call_navigate bookmark names)" unset COMPREPLY
elif [[ "${COMP_WORDS[1]}" = "remove" ]]; then
BOOKMARKS="$(__call_navigate bookmark completions)"
COMPREPLY=($(compgen -W "${BOOKMARKS}" -- $CURRENT_WORD)) COMPREPLY=($(compgen -W "${BOOKMARKS}" -- $CURRENT_WORD))
fi fi
elif [[ COMP_CWORD -eq 3 ]]; then elif [[ COMP_CWORD -eq 3 ]]; then
if [[ COMP_WORDS[1] = "add" ]]; then if [[ "${COMP_WORDS[1]}" = "add" ]]; then
COMPREPLY=($(compgen -o dirnames -- $CURRENT_WORD)) COMPREPLY=($(compgen -o dirnames -- $CURRENT_WORD))
fi fi
fi fi
+3
View File
@@ -103,6 +103,9 @@ pub enum BookmarkAction {
/// remove a bookmark by name `book remove <name>` /// remove a bookmark by name `book remove <name>`
remove(BookmarkSubArgs), remove(BookmarkSubArgs),
/// remove bookmarks with invalid paths
clean,
/// get bookmarknames for shell completions /// get bookmarknames for shell completions
completions, completions,
} }
+14 -2
View File
@@ -89,15 +89,27 @@ impl Bookmarks {
} }
/// removes a the entry with key=name if it exists, then writes the bookmarks file /// removes a the entry with key=name if it exists, then writes the bookmarks file
pub fn remove_bookmark(&mut self, name: &String) -> Result<()> { pub fn remove_bookmark(&mut self, name: &String) -> Result<PathBuf> {
let path: PathBuf;
if self.bookmarks.contains_key(name) { if self.bookmarks.contains_key(name) {
_ = self.bookmarks.remove(name); path = match self.bookmarks.remove(name) {
Some(path) => path,
None => return Err(Error::other("-- those bastards, they lied to me!")),
};
self.write_bookmark_file()?; self.write_bookmark_file()?;
} else { } else {
return Err(Error::other( return Err(Error::other(
"-- bookmark requested to delete does not exist", "-- bookmark requested to delete does not exist",
)); ));
} }
Ok(path)
}
///
pub fn remove_invalid_paths(&mut self) -> Result<()> {
self.bookmarks.retain(|_, path| path.is_dir());
self.write_bookmark_file()?;
Ok(()) Ok(())
} }
+8
View File
@@ -25,6 +25,14 @@ pub struct Config {
#[derive(Debug, Clone, Default, ConfigParser)] #[derive(Debug, Clone, Default, ConfigParser)]
pub struct GeneralSettings { pub struct GeneralSettings {
/// (bool) delete all older occurences of paths in the stack
#[default_value(false)]
pub dedup_stack: bool,
/// (bool) rotate stack to chosen path when jumping to stack entry
#[default_value(false)]
pub rotate_stack_on_jump_to_entry: bool,
/// (bool) show stack when pushing a path to the stack /// (bool) show stack when pushing a path to the stack
#[default_value(false)] #[default_value(false)]
pub show_stack_on_push: bool, pub show_stack_on_push: bool,
+16 -6
View File
@@ -58,7 +58,7 @@ fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
}; };
let mut stack = match Stack::new(args.pid) { let mut stack = match Stack::new(&config, args.pid) {
Ok(stack) => stack, Ok(stack) => stack,
Err(_) => { Err(_) => {
output.push_error(&"-- failed to build stack".to_string()); output.push_error(&"-- failed to build stack".to_string());
@@ -104,7 +104,12 @@ fn handle_push(args: &PushArgs, config: &Config, stack: &mut Stack, output: &mut
Ok(value) => value, Ok(value) => value,
Err(_) => return Err(Error::other("-- push : failed to convert path argument to number")), Err(_) => return Err(Error::other("-- push : failed to convert path argument to number")),
}; };
stack.get_entry_by_number(number)?.to_path_buf() let path = stack.get_entry_by_number(number)?.to_path_buf();
if config.general.rotate_stack_on_jump_to_entry {
stack.rotate_stack(number)?;
}
_ = stack.pop_entry(None);
path
} else { } else {
match PathBuf::from_str(&path_string) { match PathBuf::from_str(&path_string) {
Ok(value) => value, Ok(value) => value,
@@ -163,6 +168,7 @@ fn handle_bookmark(args: &BookmarkArgs, config: &Config, bookmarks: &mut Bookmar
BookmarkAction::list => list_bookmarks(config, bookmarks, output)?, BookmarkAction::list => list_bookmarks(config, bookmarks, output)?,
BookmarkAction::add(args) => add_bookmarks(args, config, bookmarks, output)?, BookmarkAction::add(args) => add_bookmarks(args, config, bookmarks, output)?,
BookmarkAction::remove(args) => remove_bookmarks(args, config, bookmarks, output)?, BookmarkAction::remove(args) => remove_bookmarks(args, config, bookmarks, output)?,
BookmarkAction::clean => bookmarks.remove_invalid_paths()?,
BookmarkAction::completions => println!("echo '{}'", bookmarks.get_bookmark_names()), BookmarkAction::completions => println!("echo '{}'", bookmarks.get_bookmark_names()),
}; };
} else if args.name.is_some() { // handle `change to bookmark` } else if args.name.is_some() { // handle `change to bookmark`
@@ -190,8 +196,12 @@ fn add_bookmarks(args: &BookmarkSubArgs, config: &Config, bookmarks: &mut Bookma
Ok(value) => value, Ok(value) => value,
Err(error) => return Err(Error::other(error.to_string())), Err(error) => return Err(Error::other(error.to_string())),
}; };
if args.name == "add" || args.name == "remove" { if args.name == "add"
return Err(Error::other("-- `add` & `remove` are subcommands and cant be used as bookmarknames")); || args.name == "remove"
|| args.name == "clean"
|| args.name == "help"
|| args.name == "completions" {
return Err(Error::other("-- `add`, `remove`, `clean`, `help` and `completions` are subcommands and cant be used as bookmarknames"));
} }
bookmarks.add_bookmark(&args.name, &path)?; bookmarks.add_bookmark(&args.name, &path)?;
@@ -208,12 +218,12 @@ fn add_bookmarks(args: &BookmarkSubArgs, config: &Config, bookmarks: &mut Bookma
} }
fn remove_bookmarks(args: &BookmarkSubArgs, config: &Config, bookmarks: &mut Bookmarks, output: &mut Output) -> Result<()> { fn remove_bookmarks(args: &BookmarkSubArgs, config: &Config, bookmarks: &mut Bookmarks, output: &mut Output) -> Result<()> {
bookmarks.remove_bookmark(&args.name)?; let path = bookmarks.remove_bookmark(&args.name)?;
if config.general.show_entries_on_bookmark { if config.general.show_entries_on_bookmark {
output.push_info(&bookmarks.to_formatted_string(config)?); output.push_info(&bookmarks.to_formatted_string(config)?);
} else { } else {
output.push_info(&format!("remove bookmark `{}{}{}`.", generate_style_sequence(Some(STYLES.set.bold), None, None), args.name, RESET_SEQ)); output.push_info(&format!("removed bookmark `{}{}{}{}{}`.", generate_style_sequence(Some(STYLES.set.bold), None, None), args.name, config.format.bookmarks_separator, path.to_str().unwrap(), RESET_SEQ));
} }
Ok(()) Ok(())
} }
+37 -3
View File
@@ -21,13 +21,13 @@ pub struct Stack {
impl Stack { impl Stack {
const STACK_FILE_DIRECTORY: &str = "/tmp/navigate/"; const STACK_FILE_DIRECTORY: &str = "/tmp/navigate/";
pub fn new(process_id: u32) -> Result<Self> { pub fn new(config: &Config, process_id: u32) -> Result<Self> {
let mut stack: Stack = Stack { let mut stack: Stack = Stack {
pid: process_id, pid: process_id,
path: PathBuf::new(), path: PathBuf::new(),
stack: Vec::<PathBuf>::new(), stack: Vec::<PathBuf>::new(),
}; };
stack.build_stack()?; stack.build_stack(config)?;
Ok(stack) Ok(stack)
} }
@@ -138,8 +138,25 @@ impl Stack {
} }
} }
/// rotate stack so that the <entry_number> is the latest
/// entry (first to be popped)
pub fn rotate_stack(&mut self, entry_number: usize) -> Result<()> {
if 0 == entry_number {
return Ok(());
} else if self.stack.len() <= entry_number {
return Err(Error::other("-- number to rotate is greater than the stacks length"));
}
let mut rotated_stack: Vec<PathBuf> = self.stack.drain(self.stack.len() - entry_number..).collect();
rotated_stack.extend(self.stack.drain(..));
self.stack = rotated_stack;
Ok(())
}
/// clean up dead stack files, parse and build stack /// clean up dead stack files, parse and build stack
fn build_stack(&mut self) -> Result<()> { fn build_stack(&mut self, config: &Config) -> Result<()> {
let stack_dir: PathBuf = match PathBuf::from_str(Self::STACK_FILE_DIRECTORY) { let stack_dir: PathBuf = match PathBuf::from_str(Self::STACK_FILE_DIRECTORY) {
Ok(value) => value, Ok(value) => value,
Err(_) => { Err(_) => {
@@ -187,6 +204,10 @@ impl Stack {
} }
self.cleanup_stack(); self.cleanup_stack();
if config.general.dedup_stack {
self.dedup_stack();
}
Ok(()) Ok(())
} }
@@ -208,6 +229,19 @@ impl Stack {
} }
} }
/// keep only the newest occurence of a path
fn dedup_stack(&mut self) {
let mut deduped_stack: Vec<PathBuf> = Vec::new();
while !self.stack.is_empty() {
let entry: PathBuf = self.stack.remove(0);
if !self.stack.contains(&entry) {
deduped_stack.push(entry);
}
}
self.stack = deduped_stack;
}
/// write stack current stack to file to save it for next execution /// write stack current stack to file to save it for next execution
fn write_stack_file(&mut self) -> Result<()> { fn write_stack_file(&mut self) -> Result<()> {
let mut output = Vec::<&str>::new(); let mut output = Vec::<&str>::new();