Skip to content

Commit

Permalink
feat: add PowerShell module
Browse files Browse the repository at this point in the history
This adds PowerShell support by invoking the following expression:
    atuin init powershell | Out-String | Invoke-Expression

Co-authored-by: Jason Shirk <[email protected]>
  • Loading branch information
ltrzesniewski and lzybkr committed Jan 12, 2025
1 parent 05aec6f commit 567ee37
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 1 deletion.
5 changes: 5 additions & 0 deletions crates/atuin-common/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
16 changes: 16 additions & 0 deletions crates/atuin/src/command/client/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use eyre::{Result, WrapErr};

mod bash;
mod fish;
mod powershell;
mod xonsh;
mod zsh;

Expand All @@ -24,6 +25,7 @@ pub struct Cmd {
}

#[derive(Clone, Copy, ValueEnum, Debug)]
#[value(rename_all = "lower")]
pub enum Shell {
/// Zsh setup
Zsh,
Expand All @@ -35,6 +37,8 @@ pub enum Shell {
Nu,
/// Xonsh setup
Xonsh,
/// PowerShell setup
PowerShell,
}

impl Cmd {
Expand Down Expand Up @@ -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);
}
};
}

Expand Down Expand Up @@ -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(())
Expand Down
40 changes: 40 additions & 0 deletions crates/atuin/src/command/client/init/powershell.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
10 changes: 9 additions & 1 deletion crates/atuin/src/command/client/search/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HistoryStats> = None;
let accept;
let result = 'render: loop {
Expand Down Expand Up @@ -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;
}
Expand Down
144 changes: 144 additions & 0 deletions crates/atuin/src/shell/atuin.ps1
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 567ee37

Please sign in to comment.