From d50745e4ea29d9e3d5f7a891498b4e7a4f66a6d1 Mon Sep 17 00:00:00 2001 From: Lucas Trzesniewski Date: Sun, 12 Jan 2025 20:44:47 +0100 Subject: [PATCH 1/3] 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 --- crates/atuin-common/src/utils.rs | 5 + crates/atuin-daemon/src/server.rs | 1 + crates/atuin/src/command/client/init.rs | 11 ++ .../src/command/client/init/powershell.rs | 20 +++ .../src/command/client/search/interactive.rs | 14 +- crates/atuin/src/shell/atuin.ps1 | 143 ++++++++++++++++++ 6 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 crates/atuin/src/command/client/init/powershell.rs create mode 100644 crates/atuin/src/shell/atuin.ps1 diff --git a/crates/atuin-common/src/utils.rs b/crates/atuin-common/src/utils.rs index 9a84c31ba9d..00043713635 100644 --- a/crates/atuin-common/src/utils.rs +++ b/crates/atuin-common/src/utils.rs @@ -138,6 +138,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-daemon/src/server.rs b/crates/atuin-daemon/src/server.rs index efed0ee3a41..1853d7befd4 100644 --- a/crates/atuin-daemon/src/server.rs +++ b/crates/atuin-daemon/src/server.rs @@ -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; diff --git a/crates/atuin/src/command/client/init.rs b/crates/atuin/src/command/client/init.rs index 516ccd263cf..ebab7433c30 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,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, @@ -35,6 +38,8 @@ pub enum Shell { Nu, /// Xonsh setup Xonsh, + /// PowerShell setup + PowerShell, } impl Cmd { @@ -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); + } } } @@ -153,6 +161,9 @@ $env.config = ( ) .await?; } + Shell::PowerShell => { + powershell::init_static(self.disable_up_arrow, self.disable_ctrl_r); + } } 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..4c442971470 --- /dev/null +++ b/crates/atuin/src/command/client/init/powershell.rs @@ -0,0 +1,20 @@ +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" } +} diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs index 0fd7cbb6acf..0925975cc2a 100644 --- a/crates/atuin/src/command/client/search/interactive.rs +++ b/crates/atuin/src/command/client/search/interactive.rs @@ -34,8 +34,7 @@ use ratatui::{ cursor::SetCursorStyle, event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, - KeyboardEnhancementFlags, MouseEvent, PopKeyboardEnhancementFlags, - PushKeyboardEnhancementFlags, + MouseEvent, }, execute, terminal, }, @@ -46,6 +45,11 @@ use ratatui::{ widgets::{Block, BorderType, Borders, Padding, Paragraph, Tabs, block::Title}, }; +#[cfg(not(target_os = "windows"))] +use ratatui::crossterm::event::{ + KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, +}; + const TAB_TITLES: [&str; 2] = ["Search", "Inspect"]; pub enum InputAction { @@ -1211,7 +1215,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..4ae4eb0ad11 --- /dev/null +++ b/crates/atuin/src/shell/atuin.ps1 @@ -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 From f667190fd853b2f74bf7a3bada0b07e2a6d418e3 Mon Sep 17 00:00:00 2001 From: Lucas Trzesniewski Date: Tue, 11 Feb 2025 23:42:16 +0100 Subject: [PATCH 2/3] fix(powershell): work around issue with PowerShell 7.5.0 https://github.com/PowerShell/PowerShell/issues/24986 --- crates/atuin/src/command/client/search.rs | 13 ++++++++-- crates/atuin/src/command/mod.rs | 1 + crates/atuin/src/shell/atuin.ps1 | 30 +++++++++-------------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/atuin/src/command/client/search.rs b/crates/atuin/src/command/client/search.rs index 8c864e77f99..4103901ab73 100644 --- a/crates/atuin/src/command/client/search.rs +++ b/crates/atuin/src/command/client/search.rs @@ -1,4 +1,5 @@ -use std::io::{IsTerminal as _, stderr}; +use std::fs::File; +use std::io::{IsTerminal as _, Write, stderr}; use atuin_common::utils::{self, Escapable as _}; use clap::Parser; @@ -131,6 +132,10 @@ pub struct Cmd { /// Include duplicate commands in the output (non-interactive only) #[arg(long)] include_duplicates: bool, + + /// File name to write the result to (hidden from help as this is meant to be used from a script) + #[arg(long = "result-file", hide = true)] + result_file: Option, } impl Cmd { @@ -213,7 +218,11 @@ impl Cmd { if self.interactive { let item = interactive::history(&query, settings, db, &history_store, theme).await?; - if stderr().is_terminal() { + + if let Some(result_file) = self.result_file { + let mut file = File::create(result_file)?; + write!(file, "{item}")?; + } else if stderr().is_terminal() { eprintln!("{}", item.escape_control()); } else { eprintln!("{item}"); diff --git a/crates/atuin/src/command/mod.rs b/crates/atuin/src/command/mod.rs index 9581319312d..a70ab6297ca 100644 --- a/crates/atuin/src/command/mod.rs +++ b/crates/atuin/src/command/mod.rs @@ -18,6 +18,7 @@ mod external; #[derive(Subcommand)] #[command(infer_subcommands = true)] +#[allow(clippy::large_enum_variant)] pub enum AtuinCmd { #[cfg(feature = "client")] #[command(flatten)] diff --git a/crates/atuin/src/shell/atuin.ps1 b/crates/atuin/src/shell/atuin.ps1 index 4ae4eb0ad11..143352b41ab 100644 --- a/crates/atuin/src/shell/atuin.ps1 +++ b/crates/atuin/src/shell/atuin.ps1 @@ -63,29 +63,21 @@ New-Module -Name Atuin -ScriptBlock { 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 = "" + $previousOutputEncoding = [System.Console]::OutputEncoding $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 + $line = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null) + + # Atuin is started through Start-Process to avoid interfering with the current shell. + $env:ATUIN_SHELL_POWERSHELL = "true" + $argString = "search -i --result-file ""$resultFile"" $ExtraArgs -- $line" + Start-Process -Wait -NoNewWindow -FilePath atuin -ArgumentList $argString + $suggestion = (Get-Content -Raw $resultFile -Encoding UTF8 | Out-String).Trim() + # 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) @@ -108,6 +100,8 @@ New-Module -Name Atuin -ScriptBlock { } finally { [System.Console]::OutputEncoding = $previousOutputEncoding + $env:ATUIN_SHELL_POWERSHELL = $null + Remove-Item $resultFile } } From 84379cd70fa1e708b762f81bfbcc757fc7d8c852 Mon Sep 17 00:00:00 2001 From: Lucas Trzesniewski Date: Sun, 2 Mar 2025 16:29:41 +0100 Subject: [PATCH 3/3] feat(powershell): add dotfiles support --- crates/atuin-dotfiles/src/shell.rs | 1 + crates/atuin-dotfiles/src/shell/powershell.rs | 169 ++++++++++++++++++ crates/atuin-dotfiles/src/store.rs | 40 ++++- crates/atuin-dotfiles/src/store/var.rs | 53 ++++-- crates/atuin/src/command/client/init.rs | 8 +- .../src/command/client/init/powershell.rs | 19 ++ 6 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 crates/atuin-dotfiles/src/shell/powershell.rs diff --git a/crates/atuin-dotfiles/src/shell.rs b/crates/atuin-dotfiles/src/shell.rs index bd61aafabd4..73a9ce8cba3 100644 --- a/crates/atuin-dotfiles/src/shell.rs +++ b/crates/atuin-dotfiles/src/shell.rs @@ -8,6 +8,7 @@ use crate::store::AliasStore; pub mod bash; pub mod fish; +pub mod powershell; pub mod xonsh; pub mod zsh; diff --git a/crates/atuin-dotfiles/src/shell/powershell.rs b/crates/atuin-dotfiles/src/shell/powershell.rs new file mode 100644 index 00000000000..7ff2a6f8f5b --- /dev/null +++ b/crates/atuin-dotfiles/src/shell/powershell.rs @@ -0,0 +1,169 @@ +use crate::shell::{Alias, Var}; +use crate::store::{AliasStore, var::VarStore}; +use std::path::PathBuf; + +async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String { + match tokio::fs::read_to_string(path).await { + Ok(aliases) => aliases, + Err(r) => { + // we failed to read the file for some reason, but the file does exist + // fallback to generating new aliases on the fly + + store.powershell().await.unwrap_or_else(|e| { + format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",) + }) + } + } +} + +async fn cached_vars(path: PathBuf, store: &VarStore) -> String { + match tokio::fs::read_to_string(path).await { + Ok(vars) => vars, + Err(r) => { + // we failed to read the file for some reason, but the file does exist + // fallback to generating new vars on the fly + + store.powershell().await.unwrap_or_else(|e| { + format!("echo 'Atuin: failed to read and generate vars: \n{r}\n{e}'",) + }) + } + } +} + +/// Return powershell dotfile config +/// +/// Do not return an error. We should not prevent the shell from starting. +/// +/// In the worst case, Atuin should not function but the shell should start correctly. +/// +/// While currently this only returns aliases, it will be extended to also return other synced dotfiles +pub async fn alias_config(store: &AliasStore) -> String { + // First try to read the cached config + let aliases = atuin_common::utils::dotfiles_cache_dir().join("aliases.ps1"); + + if aliases.exists() { + return cached_aliases(aliases, store).await; + } + + if let Err(e) = store.build().await { + return format!("echo 'Atuin: failed to generate aliases: {}'", e); + } + + cached_aliases(aliases, store).await +} + +pub async fn var_config(store: &VarStore) -> String { + // First try to read the cached config + let vars = atuin_common::utils::dotfiles_cache_dir().join("vars.ps1"); + + if vars.exists() { + return cached_vars(vars, store).await; + } + + if let Err(e) = store.build().await { + return format!("echo 'Atuin: failed to generate vars: {}'", e); + } + + cached_vars(vars, store).await +} + +pub fn format_alias(alias: &Alias) -> String { + // Set-Alias doesn't support adding implicit arguments, so use a function. + // See https://github.com/PowerShell/PowerShell/issues/12962 + + let mut result = secure_command(&format!( + "function {} {{\n {}{} @args\n}}", + alias.name, + if alias.value.starts_with(['"', '\'']) { + "& " + } else { + "" + }, + alias.value + )); + + // This makes the file layout prettier + result.insert(0, '\n'); + result +} + +pub fn format_var(var: &Var) -> String { + secure_command(&format!( + "${}{} = '{}'", + if var.export { "env:" } else { "" }, + var.name, + var.value.replace("'", "''") + )) +} + +/// Wraps the given command in an Invoke-Expression to ensure the outer script is not halted +/// if the inner command contains a syntax error. +fn secure_command(command: &str) -> String { + format!( + "Invoke-Expression -ErrorAction Continue -Command '{}'\n", + command.replace("'", "''") + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn aliases() { + assert_eq!( + format_alias(&Alias { + name: "gp".to_string(), + value: "git push".to_string(), + }), + "\n".to_string() + + &secure_command( + "function gp { + git push @args +}" + ) + ); + + assert_eq!( + format_alias(&Alias { + name: "spc".to_string(), + value: "\"path with spaces\" arg".to_string(), + }), + "\n".to_string() + + &secure_command( + "function spc { + & \"path with spaces\" arg @args +}" + ) + ); + } + + #[test] + fn vars() { + assert_eq!( + format_var(&Var { + name: "FOO".to_owned(), + value: "bar 'baz'".to_owned(), + export: true, + }), + secure_command("$env:FOO = 'bar ''baz'''") + ); + + assert_eq!( + format_var(&Var { + name: "TEST".to_owned(), + value: "1".to_owned(), + export: false, + }), + secure_command("$TEST = '1'") + ); + } + + #[test] + fn invoke_expression() { + assert_eq!( + secure_command("echo 'foo'"), + "Invoke-Expression -ErrorAction Continue -Command 'echo ''foo'''\n" + ) + } +} diff --git a/crates/atuin-dotfiles/src/store.rs b/crates/atuin-dotfiles/src/store.rs index 01316b4e998..17597065d38 100644 --- a/crates/atuin-dotfiles/src/store.rs +++ b/crates/atuin-dotfiles/src/store.rs @@ -142,7 +142,20 @@ impl AliasStore { pub async fn posix(&self) -> Result { let aliases = self.aliases().await?; + Ok(Self::format_posix(&aliases)) + } + + pub async fn xonsh(&self) -> Result { + let aliases = self.aliases().await?; + Ok(Self::format_xonsh(&aliases)) + } + pub async fn powershell(&self) -> Result { + let aliases = self.aliases().await?; + Ok(Self::format_powershell(&aliases)) + } + + fn format_posix(aliases: &[Alias]) -> String { let mut config = String::new(); for alias in aliases { @@ -153,28 +166,39 @@ impl AliasStore { config.push_str(&format!("alias {}='{}'\n", alias.name, value)); } - Ok(config) + config } - pub async fn xonsh(&self) -> Result { - let aliases = self.aliases().await?; - + fn format_xonsh(aliases: &[Alias]) -> String { let mut config = String::new(); for alias in aliases { config.push_str(&format!("aliases['{}'] ='{}'\n", alias.name, alias.value)); } - Ok(config) + config + } + + fn format_powershell(aliases: &[Alias]) -> String { + let mut config = String::new(); + + for alias in aliases { + config.push_str(&crate::shell::powershell::format_alias(alias)); + } + + config } pub async fn build(&self) -> Result<()> { let dir = atuin_common::utils::dotfiles_cache_dir(); tokio::fs::create_dir_all(dir.clone()).await?; + let aliases = self.aliases().await?; + // Build for all supported shells - let posix = self.posix().await?; - let xonsh = self.xonsh().await?; + let posix = Self::format_posix(&aliases); + let xonsh = Self::format_xonsh(&aliases); + let powershell = Self::format_powershell(&aliases); // All the same contents, maybe optimize in the future or perhaps there will be quirks // per-shell @@ -183,11 +207,13 @@ impl AliasStore { let bash = dir.join("aliases.bash"); let fish = dir.join("aliases.fish"); let xsh = dir.join("aliases.xsh"); + let ps1 = dir.join("aliases.ps1"); tokio::fs::write(zsh, &posix).await?; tokio::fs::write(bash, &posix).await?; tokio::fs::write(fish, &posix).await?; tokio::fs::write(xsh, &xonsh).await?; + tokio::fs::write(ps1, &powershell).await?; Ok(()) } diff --git a/crates/atuin-dotfiles/src/store/var.rs b/crates/atuin-dotfiles/src/store/var.rs index 76f7d66622f..c78b4153e17 100644 --- a/crates/atuin-dotfiles/src/store/var.rs +++ b/crates/atuin-dotfiles/src/store/var.rs @@ -117,31 +117,45 @@ impl VarStore { pub async fn xonsh(&self) -> Result { let env = self.vars().await?; + Ok(Self::format_xonsh(&env)) + } + pub async fn fish(&self) -> Result { + let env = self.vars().await?; + Ok(Self::format_fish(&env)) + } + + pub async fn posix(&self) -> Result { + let env = self.vars().await?; + Ok(Self::format_posix(&env)) + } + + pub async fn powershell(&self) -> Result { + let env = self.vars().await?; + Ok(Self::format_powershell(&env)) + } + + fn format_xonsh(env: &[Var]) -> String { let mut config = String::new(); for env in env { config.push_str(&format!("${}={}\n", env.name, env.value)); } - Ok(config) + config } - pub async fn fish(&self) -> Result { - let env = self.vars().await?; - + fn format_fish(env: &[Var]) -> String { let mut config = String::new(); for env in env { config.push_str(&format!("set -gx {} {}\n", env.name, env.value)); } - Ok(config) + config } - pub async fn posix(&self) -> Result { - let env = self.vars().await?; - + fn format_posix(env: &[Var]) -> String { let mut config = String::new(); for env in env { @@ -152,17 +166,30 @@ impl VarStore { } } - Ok(config) + config + } + + fn format_powershell(env: &[Var]) -> String { + let mut config = String::new(); + + for var in env { + config.push_str(&crate::shell::powershell::format_var(var)); + } + + config } pub async fn build(&self) -> Result<()> { let dir = atuin_common::utils::dotfiles_cache_dir(); tokio::fs::create_dir_all(dir.clone()).await?; + let env = self.vars().await?; + // Build for all supported shells - let posix = self.posix().await?; - let xonsh = self.xonsh().await?; - let fsh = self.fish().await?; + let posix = Self::format_posix(&env); + let xonsh = Self::format_xonsh(&env); + let fsh = Self::format_fish(&env); + let powershell = Self::format_powershell(&env); // All the same contents, maybe optimize in the future or perhaps there will be quirks // per-shell @@ -171,11 +198,13 @@ impl VarStore { let bash = dir.join("vars.bash"); let fish = dir.join("vars.fish"); let xsh = dir.join("vars.xsh"); + let ps1 = dir.join("vars.ps1"); tokio::fs::write(zsh, &posix).await?; tokio::fs::write(bash, &posix).await?; tokio::fs::write(fish, &fsh).await?; tokio::fs::write(xsh, &xonsh).await?; + tokio::fs::write(ps1, &powershell).await?; Ok(()) } diff --git a/crates/atuin/src/command/client/init.rs b/crates/atuin/src/command/client/init.rs index ebab7433c30..410f9b6a446 100644 --- a/crates/atuin/src/command/client/init.rs +++ b/crates/atuin/src/command/client/init.rs @@ -162,7 +162,13 @@ $env.config = ( .await?; } Shell::PowerShell => { - powershell::init_static(self.disable_up_arrow, self.disable_ctrl_r); + powershell::init( + alias_store, + var_store, + self.disable_up_arrow, + self.disable_ctrl_r, + ) + .await?; } } diff --git a/crates/atuin/src/command/client/init/powershell.rs b/crates/atuin/src/command/client/init/powershell.rs index 4c442971470..7d1b88766a2 100644 --- a/crates/atuin/src/command/client/init/powershell.rs +++ b/crates/atuin/src/command/client/init/powershell.rs @@ -1,3 +1,5 @@ +use atuin_dotfiles::store::{AliasStore, var::VarStore}; + pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) { let base = include_str!("../../../shell/atuin.ps1"); @@ -15,6 +17,23 @@ pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) { ); } +pub async fn init( + aliases: AliasStore, + vars: VarStore, + disable_up_arrow: bool, + disable_ctrl_r: bool, +) -> eyre::Result<()> { + init_static(disable_up_arrow, disable_ctrl_r); + + let aliases = atuin_dotfiles::shell::powershell::alias_config(&aliases).await; + let vars = atuin_dotfiles::shell::powershell::var_config(&vars).await; + + println!("{aliases}"); + println!("{vars}"); + + Ok(()) +} + fn ps_bool(value: bool) -> &'static str { if value { "$true" } else { "$false" } }