diff --git a/crates/atuin-common/src/utils.rs b/crates/atuin-common/src/utils.rs index 7f156d77ef1..73296fb0822 100644 --- a/crates/atuin-common/src/utils.rs +++ b/crates/atuin-common/src/utils.rs @@ -133,6 +133,11 @@ pub fn is_xonsh() -> bool { env::var("ATUIN_SHELL_XONSH").is_ok() } +pub fn is_powershell() -> bool { + // only set on powershell + env::var("ATUIN_SHELL_POWERSHELL").is_ok() +} + /// Extension trait for anything that can behave like a string to make it easy to escape control /// characters. /// diff --git a/crates/atuin/src/command/client/init.rs b/crates/atuin/src/command/client/init.rs index 8238a69bcf7..b4f1cc03daa 100644 --- a/crates/atuin/src/command/client/init.rs +++ b/crates/atuin/src/command/client/init.rs @@ -7,6 +7,7 @@ use eyre::{Result, WrapErr}; mod bash; mod fish; +mod powershell; mod xonsh; mod zsh; @@ -24,6 +25,7 @@ pub struct Cmd { } #[derive(Clone, Copy, ValueEnum, Debug)] +#[value(rename_all = "lower")] pub enum Shell { /// Zsh setup Zsh, @@ -35,6 +37,8 @@ pub enum Shell { Nu, /// Xonsh setup Xonsh, + /// PowerShell setup + PowerShell, } impl Cmd { @@ -100,6 +104,9 @@ $env.config = ( Shell::Xonsh => { xonsh::init_static(self.disable_up_arrow, self.disable_ctrl_r); } + Shell::PowerShell => { + powershell::init_static(self.disable_up_arrow, self.disable_ctrl_r); + } }; } @@ -153,6 +160,15 @@ $env.config = ( ) .await?; } + Shell::PowerShell => { + powershell::init( + alias_store, + var_store, + self.disable_up_arrow, + self.disable_ctrl_r, + ) + .await?; + } } Ok(()) diff --git a/crates/atuin/src/command/client/init/powershell.rs b/crates/atuin/src/command/client/init/powershell.rs new file mode 100644 index 00000000000..f87a75a00b0 --- /dev/null +++ b/crates/atuin/src/command/client/init/powershell.rs @@ -0,0 +1,40 @@ +use atuin_dotfiles::store::{var::VarStore, AliasStore}; +use eyre::Result; + +pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) { + let base = include_str!("../../../shell/atuin.ps1"); + + let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() { + (false, false) + } else { + (!disable_ctrl_r, !disable_up_arrow) + }; + + fn bool(value: bool) -> &'static str { + if value { + "$true" + } else { + "$false" + } + } + + println!("{base}"); + println!( + "Enable-AtuinSearchKeys -CtrlR {} -UpArrow {}", + bool(bind_ctrl_r), + bool(bind_up_arrow) + ); +} + +pub async fn init( + _aliases: AliasStore, + _vars: VarStore, + disable_up_arrow: bool, + disable_ctrl_r: bool, +) -> Result<()> { + init_static(disable_up_arrow, disable_ctrl_r); + + // dotfiles are not supported yet + + Ok(()) +} diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs index c1a678f0872..10660804f07 100644 --- a/crates/atuin/src/command/client/search/interactive.rs +++ b/crates/atuin/src/command/client/search/interactive.rs @@ -1098,6 +1098,10 @@ pub async fn history( let mut results = app.query_results(&mut db, settings.smart_sort).await?; + if settings.inline_height > 0 { + terminal.clear()?; + } + let mut stats: Option = None; let accept; let result = 'render: loop { @@ -1180,7 +1184,11 @@ pub async fn history( 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() || utils::is_xonsh()) + && (utils::is_zsh() + || utils::is_fish() + || utils::is_bash() + || utils::is_xonsh() + || utils::is_powershell()) { command = String::from("__atuin_accept__:") + &command; } diff --git a/crates/atuin/src/shell/atuin.ps1 b/crates/atuin/src/shell/atuin.ps1 new file mode 100644 index 00000000000..82e8752ebfc --- /dev/null +++ b/crates/atuin/src/shell/atuin.ps1 @@ -0,0 +1,144 @@ +# Atuin PowerShell module +# +# Usage: atuin init powershell | Out-String | Invoke-Expression + +if (Get-Module Atuin -ErrorAction Ignore) { + Write-Warning "The Atuin module is already loaded." + return +} + +if (!(Get-Command atuin -ErrorAction Ignore)) { + Write-Error "The 'atuin' executable needs to be available in the PATH." + return +} + +if (!(Get-Module PSReadLine -ErrorAction Ignore)) { + Write-Error "Atuin requires the PSReadLine module to be installed." + return +} + +New-Module -Name Atuin -ScriptBlock { + $env:ATUIN_SESSION = atuin uuid + + $script:atuinHistoryId = $null + $script:previousPSConsoleHostReadLine = $Function:PSConsoleHostReadLine + + # The ReadLine overloads changed with breaking changes over time, make sure the one we expect is available. + $script:hasExpectedReadLineOverload = ([Microsoft.PowerShell.PSConsoleReadLine]::ReadLine).OverloadDefinitions.Contains("static string ReadLine(runspace runspace, System.Management.Automation.EngineIntrinsics engineIntrinsics, System.Threading.CancellationToken cancellationToken, System.Nullable[bool] lastRunStatus)") + + function PSConsoleHostReadLine { + # This needs to be done as the first thing because any script run will flush $?. + $lastRunStatus = $? + + # Exit statuses are maintained separately for native and PowerShell commands, this needs to be taken into account. + $exitCode = if ($lastRunStatus) { 0 } elseif ($global:LASTEXITCODE) { $global:LASTEXITCODE } else { 1 } + + if ($script:atuinHistoryId) { + # The duration is not recorded in old PowerShell versions, let Atuin handle it. + $duration = (Get-History -Count 1).Duration.Ticks * 100 + $durationArg = if ($duration) { "--duration=$duration" } else { "" } + + atuin history end --exit=$exitCode $durationArg -- $script:atuinHistoryId | Out-Null + + $global:LASTEXITCODE = $exitCode + $script:atuinHistoryId = $null + } + + # PSConsoleHostReadLine implementation from PSReadLine, adjusted to support old versions. + Microsoft.PowerShell.Core\Set-StrictMode -Off + + $line = if ($script:hasExpectedReadLineOverload) { + # When the overload we expect is available, we can pass $lastRunStatus to it. + [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($Host.Runspace, $ExecutionContext, [System.Threading.CancellationToken]::None, $lastRunStatus) + } else { + # Either PSReadLine is older than v2.2.0-beta3, or maybe newer than we expect, so use the function from PSReadLine as-is. + & $script:previousPSConsoleHostReadLine + } + + $script:atuinHistoryId = atuin history start -- $line + + return $line + } + + function RunSearch { + param([string]$ExtraArgs = "") + + $line = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) + + # Atuin is started through Start-Process to avoid interfering with the current shell, + # and to capture its output which is provided in stderr (redirected to a temporary file). + + $suggestion = "" + $resultFile = New-TemporaryFile + try { + $env:ATUIN_SHELL_POWERSHELL = "true" + $argString = "search -i $ExtraArgs -- $line" + Start-Process -Wait -NoNewWindow -RedirectStandardError $resultFile.FullName -FilePath atuin -ArgumentList $argString + $suggestion = (Get-Content -Raw $resultFile | Out-String).Trim() + } + finally { + $env:ATUIN_SHELL_POWERSHELL = $null + Remove-Item $resultFile + } + + $previousOutputEncoding = [System.Console]::OutputEncoding + try { + [System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + + # PSReadLine maintains its own cursor position, which will no longer be valid if Atuin scrolls the display in inline mode. + # Fortunately, InvokePrompt can receive a new Y position and reset the internal state. + [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt($null, $Host.UI.RawUI.CursorPosition.Y) + } + finally { + [System.Console]::OutputEncoding = $previousOutputEncoding + } + + if ($suggestion -eq "") { + # The previous input was already rendered by InvokePrompt + return + } + + $acceptPrefix = "__atuin_accept__:" + + if ( $suggestion.StartsWith($acceptPrefix)) { + [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() + [Microsoft.PowerShell.PSConsoleReadLine]::Insert($suggestion.Substring($acceptPrefix.Length)) + [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() + } else { + [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() + [Microsoft.PowerShell.PSConsoleReadLine]::Insert($suggestion) + } + } + + function Enable-AtuinSearchKeys { + param([bool]$CtrlR = $true, [bool]$UpArrow = $true) + + if ($CtrlR) { + Set-PSReadLineKeyHandler -Chord "Ctrl+r" -BriefDescription "Runs Atuin search" -ScriptBlock { + RunSearch + } + } + + if ($UpArrow) { + Set-PSReadLineKeyHandler -Chord "UpArrow" -BriefDescription "Runs Atuin search" -ScriptBlock { + $line = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null) + + if (!$line.Contains("`n")) { + RunSearch -ExtraArgs "--shell-up-key-binding" + } else { + [Microsoft.PowerShell.PSConsoleReadLine]::PreviousLine() + } + } + } + } + + $ExecutionContext.SessionState.Module.OnRemove += { + $env:ATUIN_SESSION = $null + $Function:PSConsoleHostReadLine = $script:previousPSConsoleHostReadLine + } + + Export-ModuleMember -Function @("Enable-AtuinSearchKeys", "PSConsoleHostReadLine") +} | Import-Module -Global