diff --git a/README.md b/README.md index 1b893f49..dff561e2 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ I wanted to. And I **really** don't want to. - bash - fish - nushell +- xonsh ## Community @@ -327,6 +328,14 @@ Add to `config.nu`: source ~/.local/share/atuin/init.nu ``` +### Xonsh + +Add +``` +execx($(atuin init xonsh)) +``` +to the end of your `~/.xonshrc` + # Contributors diff --git a/atuin-common/src/utils.rs b/atuin-common/src/utils.rs index 87c4b14c..889c7811 100644 --- a/atuin-common/src/utils.rs +++ b/atuin-common/src/utils.rs @@ -101,6 +101,11 @@ pub fn is_bash() -> bool { env::var("ATUIN_SHELL_BASH").is_ok() } +pub fn is_xonsh() -> bool { + // only set on xonsh + env::var("ATUIN_SHELL_XONSH").is_ok() +} + /// Extension trait for anything that can behave like a string to make it easy to escape control /// characters. /// diff --git a/atuin/src/command/client/search/interactive.rs b/atuin/src/command/client/search/interactive.rs index 1fc37b57..27c82f2c 100644 --- a/atuin/src/command/client/search/interactive.rs +++ b/atuin/src/command/client/search/interactive.rs @@ -967,7 +967,9 @@ pub async fn history( match result { InputAction::Accept(index) if index < results.len() => { let mut command = results.swap_remove(index).command; - if accept && (utils::is_zsh() || utils::is_fish() || utils::is_bash()) { + if accept + && (utils::is_zsh() || utils::is_fish() || utils::is_bash() || utils::is_xonsh()) + { command = String::from("__atuin_accept__:") + &command; } diff --git a/atuin/src/command/init.rs b/atuin/src/command/init.rs index eb841c9f..3eb50d68 100644 --- a/atuin/src/command/init.rs +++ b/atuin/src/command/init.rs @@ -23,6 +23,8 @@ pub enum Shell { Fish, /// Nu setup Nu, + /// Xonsh setup + Xonsh, } impl Cmd { @@ -140,12 +142,31 @@ bind -M insert \e\[A _atuin_bind_up"; } } + fn init_xonsh(&self) { + let base = include_str!("../shell/atuin.xsh"); + let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() { + (false, false) + } else { + (!self.disable_ctrl_r, !self.disable_up_arrow) + }; + println!( + "_ATUIN_BIND_CTRL_R={}", + if bind_ctrl_r { "True" } else { "False" } + ); + println!( + "_ATUIN_BIND_UP_ARROW={}", + if bind_up_arrow { "True" } else { "False" } + ); + println!("{base}"); + } + pub fn run(self) { match self.shell { Shell::Zsh => self.init_zsh(), Shell::Bash => self.init_bash(), Shell::Fish => self.init_fish(), Shell::Nu => self.init_nu(), + Shell::Xonsh => self.init_xonsh(), } } } diff --git a/atuin/src/shell/atuin.xsh b/atuin/src/shell/atuin.xsh new file mode 100644 index 00000000..5df26b26 --- /dev/null +++ b/atuin/src/shell/atuin.xsh @@ -0,0 +1,67 @@ +import subprocess +from prompt_toolkit.keys import Keys + +$ATUIN_SESSION=$(atuin uuid).rstrip('\n') + + +@events.on_precommand +def _atuin_precommand(cmd: str): + cmd = cmd.rstrip("\n") + $ATUIN_HISTORY_ID = $(atuin history start -- @(cmd)).rstrip("\n") + + +@events.on_postcommand +def _atuin_postcommand(cmd: str, rtn: int, out, ts): + if "ATUIN_HISTORY_ID" not in ${...}: + return + + duration = ts[1] - ts[0] + # Duration is float representing seconds, but atuin expects integer of nanoseconds + nanos = round(duration * 10 ** 9) + with ${...}.swap(ATUIN_LOG="error"): + # This causes the entire .xonshrc to be re-executed, which is incredibly slow + # This happens when using a subshell and using output redirection at the same time + # For more details, see https://github.com/xonsh/xonsh/issues/5224 + # (atuin history end --exit @(rtn) -- $ATUIN_HISTORY_ID &) > /dev/null 2>&1 + atuin history end --exit @(rtn) --duration @(nanos) -- $ATUIN_HISTORY_ID > /dev/null 2>&1 + del $ATUIN_HISTORY_ID + + +def _search(event, extra_args: list[str]): + buffer = event.current_buffer + cmd = ["atuin", "search", "--interactive", *extra_args, "--", buffer.text] + # We need to explicitly pass in xonsh env, in case user has set XDG_HOME or something else that matters + env = ${...}.detype() + env["ATUIN_SHELL_XONSH"] = "t" + + p = subprocess.run(cmd, stderr=subprocess.PIPE, encoding="utf-8", env=env) + result = p.stderr.rstrip("\n") + # redraw prompt - necessary if atuin is configured to run inline, rather than fullscreen + event.cli.renderer.erase() + + if not result: + return + + buffer.reset() + if result.startswith("__atuin_accept__:"): + buffer.insert_text(result[17:]) + buffer.validate_and_handle() + else: + buffer.insert_text(result) + + +@events.on_ptk_create +def _custom_keybindings(bindings, **kw): + @bindings.add(Keys.ControlR, filter=_ATUIN_BIND_CTRL_R) + def r_search(event): + _search(event, extra_args=[]) + + @bindings.add(Keys.Up, filter=_ATUIN_BIND_UP_ARROW) + def up_search(event): + # Only trigger if the buffer is a single line + if not '\n' in buffer.text: + _search(event, extra_args=["--shell-up-key-binding"]) + return + + # Run the default behavior for up arrow + event.current_buffer.auto_up(count=event.arg)