├── .env ├── README.md ├── extend-history.plugin.zsh └── stream.py /.env: -------------------------------------------------------------------------------- 1 | # locally emulate a standard installation from github 2 | install_plugin () { 3 | echo "make sure `xav-b/zsh-extend-history` is listed undedr `antigen bundles`" 4 | # TODO 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contextual shell history - Zsh Plugin 2 | 3 | ### Install 4 | 5 | - Antigen: `antigen bundle xav-b/zsh-extend-history` 6 | - ZPlug: `zplug "xav-b/zsh-extend-history"` 7 | - oh-my-zsh: 8 | 9 | ```Shell 10 | $ git clone https://github.com/xav-b/zsh-extend-history ~/.oh-my-zsh/custom/plugins/extend-history 11 | ``` 12 | 13 | And add `extend-history` to `plugins` in `.zshrc`. 14 | 15 | 16 | ### Configuration 17 | 18 | ```Zsh 19 | # file to write history 20 | # default to `$HOME/.zsh_extended_history` 21 | export ZSH_EXTEND_HISTORY_FILE="/tmp/my-zsh.history" 22 | 23 | # print history collected on stdout instead of file 24 | export ZSH_EXTEND_HISTORY_DEBUG="true" 25 | ``` 26 | 27 | 28 | ### Development 29 | 30 | Just source the file everytime you test changes =) 31 | 32 | Something like that can help: `tail -f $ZSH_EXTEND_HISTORY_FILE | ./stream.py` 33 | 34 | 35 | ### Ideas/Notes 36 | 37 | - vim and other long-lasting commands is an issue for the start and end 38 | hooks (other commands will probably happen in the meantime). We should 39 | instead compute a unique id that the `end` can refer too, and use it 40 | to retrieve the start stored in a temporary place (/tmp/gi/{this-id}.cmd). 41 | 42 | - the `end` command sometimes appear on a seperate line (most probably 43 | related) 44 | 45 | - option to ignore "boring commands" (ls, cd, ...) 46 | - command to nicely display in the terminal? (or put that in gi) 47 | - session id 48 | - Deduplicate 49 | - Only search/display history per directory/git project 50 | -------------------------------------------------------------------------------- /extend-history.plugin.zsh: -------------------------------------------------------------------------------- 1 | # Leverage zsh hooks to enhance history recording 2 | # http://zsh.sourceforge.net/Doc/Release/Functions.html 3 | 4 | # TODO use `locale` 5 | 6 | # defaul configuration (can be overwritten) 7 | export ZSH_EXTEND_HISTORY_DEFAULT_FILE="$HOME/.zsh_extended_history" 8 | export ZSH_EXTEND_HISTORY_FILE="$ZSH_EXTEND_HISTORY_DEFAULT_FILE" 9 | 10 | g_cmd="" 11 | 12 | _check_mode() { 13 | [ -n "$ZSH_EXTEND_HISTORY_DEBUG" ] && echo "[WARN] debug mode activated on zsh history plugin" 14 | [ -n "$ZSH_EXTEND_HISTORY_DEBUG" ] && ZSH_EXTEND_HISTORY_FILE="/dev/stdout" 15 | # otherwise set it back to history file 16 | [ -z "$ZSH_EXTEND_HISTORY_DEBUG" ] && ZSH_EXTEND_HISTORY_FILE="${ZSH_EXTEND_HISTORY_FILE:-$ZSH_EXTEND_HISTORY_DEFAULT_FILE}" 17 | } 18 | 19 | _record() { 20 | g_cmd="${g_cmd};$@" 21 | } 22 | 23 | _commit() { 24 | _check_mode 25 | #printf "$@" >> ${ZSH_EXTEND_HISTORY_FILE} 26 | printf "${g_cmd}\n" >> ${ZSH_EXTEND_HISTORY_FILE} 27 | g_cmd="" 28 | } 29 | 30 | _record_project_info() { 31 | if [[ $(git rev-parse --is-inside-work-tree 2> /dev/null) == true ]]; then 32 | _record "git_branch=$(git rev-parse --abbrev-ref HEAD);git_commit=$(git rev-parse --short HEAD);git_project=$(basename -s .git `git config --get remote.origin.url`)" 33 | fi 34 | 35 | # TODO support other language than Python 36 | if [[ -n "${VIRTUAL_ENV}" ]]; then 37 | # cf https://stackoverflow.com/questions/23862569/unable-to-store-python-version-to-a-shell-variable 38 | _record "venv=$(basename ${VIRTUAL_ENV});runtime=$(python --version 2>&1)" 39 | fi 40 | } 41 | 42 | _record_terminal_info() { 43 | _record "tty=$(tty);term=${TERM};shell=${SHELL};tmux=${TMUX}" 44 | } 45 | 46 | _command_history_preexec() { 47 | # NOTE should it be global? 48 | _record "uuid=$(uuidgen | tr '[:upper:]' '[:lower:]');startts=$(date +%s);user_cmd=$1;real_cmd=$2;user=$USER;host=$(hostname);pwd=$PWD" 49 | 50 | _record_terminal_info 51 | 52 | _record_project_info 53 | } 54 | 55 | _command_history_precmd() { 56 | # Executed before each prompt. 57 | 58 | # it has to be the first line otherwise we risk to get the exit code of 59 | # another command 60 | local LAST_EXIT_CODE=$? 61 | 62 | if [[ "$g_cmd" != "" ]]; then 63 | _record "endts=$(date +%s);code=${LAST_EXIT_CODE}" 64 | _commit 65 | else 66 | # reset (I'm not sure it's necessary) 67 | g_cmd="" 68 | fi 69 | } 70 | 71 | # register hooks 72 | precmd_functions+=(_command_history_precmd) 73 | preexec_functions+=(_command_history_preexec) 74 | -------------------------------------------------------------------------------- /stream.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from pprint import pprint as pp 4 | import sys 5 | 6 | 7 | def parse(line): 8 | for kv in line.split(';')[1:]: 9 | yield kv.split('=') 10 | 11 | 12 | def cant_die(func): 13 | def _inner(*args, **kwargs): 14 | try: 15 | return func(*args, **kwargs) 16 | except Exception as e: 17 | print('failed to parse line: {}'.format(e)) 18 | return _inner 19 | 20 | 21 | @cant_die 22 | def process_history(line): 23 | print('\n=== New command ===') 24 | print(line) 25 | pp({k: v for k, v in parse(line)}, indent=4) 26 | # FIXME for some reason it doesn't work piping to `jq` 27 | # import json 28 | # sys.stdout.write(json.dumps({k: v for k, v in parse(line)})) 29 | # sys.stdout.write('\r\n') 30 | 31 | 32 | if __name__ == '__main__': 33 | # TODO handle ctrl-c 34 | while 1: 35 | line = sys.stdin.readline() 36 | if not line: 37 | continue 38 | 39 | process_history(line) 40 | --------------------------------------------------------------------------------