Skip to content

Commit 59c6f95

Browse files
ltrzesniewskilzybkr
andcommitted
feat: add PowerShell module
This adds PowerShell support by invoking the following expression: atuin init powershell | Out-String | Invoke-Expression Co-authored-by: Jason Shirk <[email protected]>
1 parent 05aec6f commit 59c6f95

File tree

7 files changed

+200
-4
lines changed

7 files changed

+200
-4
lines changed

crates/atuin-common/src/utils.rs

+5
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ pub fn is_xonsh() -> bool {
133133
env::var("ATUIN_SHELL_XONSH").is_ok()
134134
}
135135

136+
pub fn is_powershell() -> bool {
137+
// only set on powershell
138+
env::var("ATUIN_SHELL_POWERSHELL").is_ok()
139+
}
140+
136141
/// Extension trait for anything that can behave like a string to make it easy to escape control
137142
/// characters.
138143
///

crates/atuin-daemon/src/server.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use atuin_client::encryption;
44
use atuin_client::history::store::HistoryStore;
55
use atuin_client::record::sqlite_store::SqliteStore;
66
use atuin_client::settings::Settings;
7+
#[cfg(unix)]
78
use std::path::PathBuf;
89
use std::sync::Arc;
910
use time::OffsetDateTime;

crates/atuin-server/src/handlers/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl<'a> RespExt<'a> for ErrorResponse<'a> {
5656
}
5757
}
5858

59-
fn reply(reason: &'a str) -> ErrorResponse {
59+
fn reply(reason: &'a str) -> ErrorResponse<'a> {
6060
Self {
6161
reason: reason.into(),
6262
}

crates/atuin/src/command/client/init.rs

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use eyre::{Result, WrapErr};
77

88
mod bash;
99
mod fish;
10+
mod powershell;
1011
mod xonsh;
1112
mod zsh;
1213

@@ -24,6 +25,8 @@ pub struct Cmd {
2425
}
2526

2627
#[derive(Clone, Copy, ValueEnum, Debug)]
28+
#[value(rename_all = "lower")]
29+
#[allow(clippy::enum_variant_names, clippy::doc_markdown)]
2730
pub enum Shell {
2831
/// Zsh setup
2932
Zsh,
@@ -35,6 +38,8 @@ pub enum Shell {
3538
Nu,
3639
/// Xonsh setup
3740
Xonsh,
41+
/// PowerShell setup
42+
PowerShell,
3843
}
3944

4045
impl Cmd {
@@ -100,6 +105,9 @@ $env.config = (
100105
Shell::Xonsh => {
101106
xonsh::init_static(self.disable_up_arrow, self.disable_ctrl_r);
102107
}
108+
Shell::PowerShell => {
109+
powershell::init_static(self.disable_up_arrow, self.disable_ctrl_r);
110+
}
103111
};
104112
}
105113

@@ -153,6 +161,9 @@ $env.config = (
153161
)
154162
.await?;
155163
}
164+
Shell::PowerShell => {
165+
powershell::init_static(self.disable_up_arrow, self.disable_ctrl_r);
166+
}
156167
}
157168

158169
Ok(())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
2+
let base = include_str!("../../../shell/atuin.ps1");
3+
4+
let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() {
5+
(false, false)
6+
} else {
7+
(!disable_ctrl_r, !disable_up_arrow)
8+
};
9+
10+
println!("{base}");
11+
println!(
12+
"Enable-AtuinSearchKeys -CtrlR {} -UpArrow {}",
13+
ps_bool(bind_ctrl_r),
14+
ps_bool(bind_up_arrow)
15+
);
16+
}
17+
18+
fn ps_bool(value: bool) -> &'static str {
19+
if value {
20+
"$true"
21+
} else {
22+
"$false"
23+
}
24+
}

crates/atuin/src/command/client/search/interactive.rs

+15-3
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ use ratatui::{
3333
cursor::SetCursorStyle,
3434
event::{
3535
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
36-
KeyboardEnhancementFlags, MouseEvent, PopKeyboardEnhancementFlags,
37-
PushKeyboardEnhancementFlags,
36+
MouseEvent,
3837
},
3938
execute, terminal,
4039
},
@@ -46,6 +45,11 @@ use ratatui::{
4645
Frame, Terminal, TerminalOptions, Viewport,
4746
};
4847

48+
#[cfg(not(target_os = "windows"))]
49+
use ratatui::crossterm::event::{
50+
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
51+
};
52+
4953
const TAB_TITLES: [&str; 2] = ["Search", "Inspect"];
5054

5155
pub enum InputAction {
@@ -1098,6 +1102,10 @@ pub async fn history(
10981102

10991103
let mut results = app.query_results(&mut db, settings.smart_sort).await?;
11001104

1105+
if settings.inline_height > 0 {
1106+
terminal.clear()?;
1107+
}
1108+
11011109
let mut stats: Option<HistoryStats> = None;
11021110
let accept;
11031111
let result = 'render: loop {
@@ -1180,7 +1188,11 @@ pub async fn history(
11801188
InputAction::Accept(index) if index < results.len() => {
11811189
let mut command = results.swap_remove(index).command;
11821190
if accept
1183-
&& (utils::is_zsh() || utils::is_fish() || utils::is_bash() || utils::is_xonsh())
1191+
&& (utils::is_zsh()
1192+
|| utils::is_fish()
1193+
|| utils::is_bash()
1194+
|| utils::is_xonsh()
1195+
|| utils::is_powershell())
11841196
{
11851197
command = String::from("__atuin_accept__:") + &command;
11861198
}

crates/atuin/src/shell/atuin.ps1

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Atuin PowerShell module
2+
#
3+
# Usage: atuin init powershell | Out-String | Invoke-Expression
4+
5+
if (Get-Module Atuin -ErrorAction Ignore) {
6+
Write-Warning "The Atuin module is already loaded."
7+
return
8+
}
9+
10+
if (!(Get-Command atuin -ErrorAction Ignore)) {
11+
Write-Error "The 'atuin' executable needs to be available in the PATH."
12+
return
13+
}
14+
15+
if (!(Get-Module PSReadLine -ErrorAction Ignore)) {
16+
Write-Error "Atuin requires the PSReadLine module to be installed."
17+
return
18+
}
19+
20+
New-Module -Name Atuin -ScriptBlock {
21+
$env:ATUIN_SESSION = atuin uuid
22+
23+
$script:atuinHistoryId = $null
24+
$script:previousPSConsoleHostReadLine = $Function:PSConsoleHostReadLine
25+
26+
# The ReadLine overloads changed with breaking changes over time, make sure the one we expect is available.
27+
$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)")
28+
29+
function PSConsoleHostReadLine {
30+
# This needs to be done as the first thing because any script run will flush $?.
31+
$lastRunStatus = $?
32+
33+
# Exit statuses are maintained separately for native and PowerShell commands, this needs to be taken into account.
34+
$exitCode = if ($lastRunStatus) { 0 } elseif ($global:LASTEXITCODE) { $global:LASTEXITCODE } else { 1 }
35+
36+
if ($script:atuinHistoryId) {
37+
# The duration is not recorded in old PowerShell versions, let Atuin handle it.
38+
$duration = (Get-History -Count 1).Duration.Ticks * 100
39+
$durationArg = if ($duration) { "--duration=$duration" } else { "" }
40+
41+
atuin history end --exit=$exitCode $durationArg -- $script:atuinHistoryId | Out-Null
42+
43+
$global:LASTEXITCODE = $exitCode
44+
$script:atuinHistoryId = $null
45+
}
46+
47+
# PSConsoleHostReadLine implementation from PSReadLine, adjusted to support old versions.
48+
Microsoft.PowerShell.Core\Set-StrictMode -Off
49+
50+
$line = if ($script:hasExpectedReadLineOverload) {
51+
# When the overload we expect is available, we can pass $lastRunStatus to it.
52+
[Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($Host.Runspace, $ExecutionContext, [System.Threading.CancellationToken]::None, $lastRunStatus)
53+
} else {
54+
# Either PSReadLine is older than v2.2.0-beta3, or maybe newer than we expect, so use the function from PSReadLine as-is.
55+
& $script:previousPSConsoleHostReadLine
56+
}
57+
58+
$script:atuinHistoryId = atuin history start -- $line
59+
60+
return $line
61+
}
62+
63+
function RunSearch {
64+
param([string]$ExtraArgs = "")
65+
66+
$line = $null
67+
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null)
68+
69+
# Atuin is started through Start-Process to avoid interfering with the current shell,
70+
# and to capture its output which is provided in stderr (redirected to a temporary file).
71+
72+
$suggestion = ""
73+
$resultFile = New-TemporaryFile
74+
try {
75+
$env:ATUIN_SHELL_POWERSHELL = "true"
76+
$argString = "search -i $ExtraArgs -- $line"
77+
Start-Process -Wait -NoNewWindow -RedirectStandardError $resultFile.FullName -FilePath atuin -ArgumentList $argString
78+
$suggestion = (Get-Content -Raw $resultFile -Encoding UTF8 | Out-String).Trim()
79+
}
80+
finally {
81+
$env:ATUIN_SHELL_POWERSHELL = $null
82+
Remove-Item $resultFile
83+
}
84+
85+
$previousOutputEncoding = [System.Console]::OutputEncoding
86+
try {
87+
[System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8
88+
89+
# PSReadLine maintains its own cursor position, which will no longer be valid if Atuin scrolls the display in inline mode.
90+
# Fortunately, InvokePrompt can receive a new Y position and reset the internal state.
91+
[Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt($null, $Host.UI.RawUI.CursorPosition.Y + [int]$env:ATUIN_POWERSHELL_PROMPT_OFFSET)
92+
93+
if ($suggestion -eq "") {
94+
# The previous input was already rendered by InvokePrompt
95+
return
96+
}
97+
98+
$acceptPrefix = "__atuin_accept__:"
99+
100+
if ( $suggestion.StartsWith($acceptPrefix)) {
101+
[Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
102+
[Microsoft.PowerShell.PSConsoleReadLine]::Insert($suggestion.Substring($acceptPrefix.Length))
103+
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
104+
} else {
105+
[Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
106+
[Microsoft.PowerShell.PSConsoleReadLine]::Insert($suggestion)
107+
}
108+
}
109+
finally {
110+
[System.Console]::OutputEncoding = $previousOutputEncoding
111+
}
112+
}
113+
114+
function Enable-AtuinSearchKeys {
115+
param([bool]$CtrlR = $true, [bool]$UpArrow = $true)
116+
117+
if ($CtrlR) {
118+
Set-PSReadLineKeyHandler -Chord "Ctrl+r" -BriefDescription "Runs Atuin search" -ScriptBlock {
119+
RunSearch
120+
}
121+
}
122+
123+
if ($UpArrow) {
124+
Set-PSReadLineKeyHandler -Chord "UpArrow" -BriefDescription "Runs Atuin search" -ScriptBlock {
125+
$line = $null
126+
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null)
127+
128+
if (!$line.Contains("`n")) {
129+
RunSearch -ExtraArgs "--shell-up-key-binding"
130+
} else {
131+
[Microsoft.PowerShell.PSConsoleReadLine]::PreviousLine()
132+
}
133+
}
134+
}
135+
}
136+
137+
$ExecutionContext.SessionState.Module.OnRemove += {
138+
$env:ATUIN_SESSION = $null
139+
$Function:PSConsoleHostReadLine = $script:previousPSConsoleHostReadLine
140+
}
141+
142+
Export-ModuleMember -Function @("Enable-AtuinSearchKeys", "PSConsoleHostReadLine")
143+
} | Import-Module -Global

0 commit comments

Comments
 (0)