Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PowerShell module #2543

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
1 change: 1 addition & 0 deletions crates/atuin-daemon/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use atuin_client::encryption;
use atuin_client::history::store::HistoryStore;
use atuin_client::record::sqlite_store::SqliteStore;
use atuin_client::settings::Settings;
#[cfg(unix)]
use std::path::PathBuf;
use std::sync::Arc;
use time::OffsetDateTime;
Expand Down
2 changes: 1 addition & 1 deletion crates/atuin-server/src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl<'a> RespExt<'a> for ErrorResponse<'a> {
}
}

fn reply(reason: &'a str) -> ErrorResponse {
fn reply(reason: &'a str) -> ErrorResponse<'a> {
Self {
reason: reason.into(),
}
Expand Down
11 changes: 11 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,8 @@ pub struct Cmd {
}

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

impl Cmd {
Expand Down Expand Up @@ -100,6 +105,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 +161,9 @@ $env.config = (
)
.await?;
}
Shell::PowerShell => {
powershell::init_static(self.disable_up_arrow, self.disable_ctrl_r);
}
}

Ok(())
Expand Down
24 changes: 24 additions & 0 deletions crates/atuin/src/command/client/init/powershell.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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)
};

println!("{base}");
println!(
"Enable-AtuinSearchKeys -CtrlR {} -UpArrow {}",
ps_bool(bind_ctrl_r),
ps_bool(bind_up_arrow)
);
}

fn ps_bool(value: bool) -> &'static str {
if value {
"$true"
} else {
"$false"
}
}
18 changes: 15 additions & 3 deletions crates/atuin/src/command/client/search/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ use ratatui::{
cursor::SetCursorStyle,
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
KeyboardEnhancementFlags, MouseEvent, PopKeyboardEnhancementFlags,
PushKeyboardEnhancementFlags,
MouseEvent,
},
execute, terminal,
},
Expand All @@ -46,6 +45,11 @@ use ratatui::{
Frame, Terminal, TerminalOptions, Viewport,
};

#[cfg(not(target_os = "windows"))]
use ratatui::crossterm::event::{
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};

const TAB_TITLES: [&str; 2] = ["Search", "Inspect"];

pub enum InputAction {
Expand Down Expand Up @@ -1098,6 +1102,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 +1188,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
143 changes: 143 additions & 0 deletions crates/atuin/src/shell/atuin.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# 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
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null)

# 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 -Encoding UTF8 | 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 + [int]$env:ATUIN_POWERSHELL_PROMPT_OFFSET)

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)
}
}
finally {
[System.Console]::OutputEncoding = $previousOutputEncoding
}
}

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
Loading