Skip to content

Commit 17b7319

Browse files
committed
feat(powershell): add dotfiles support
1 parent 21bb83a commit 17b7319

File tree

6 files changed

+233
-20
lines changed

6 files changed

+233
-20
lines changed

Diff for: crates/atuin-dotfiles/src/shell.rs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::store::AliasStore;
88

99
pub mod bash;
1010
pub mod fish;
11+
pub mod powershell;
1112
pub mod xonsh;
1213
pub mod zsh;
1314

Diff for: crates/atuin-dotfiles/src/shell/powershell.rs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use std::path::PathBuf;
2+
3+
use crate::store::{var::VarStore, AliasStore};
4+
5+
async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
6+
match tokio::fs::read_to_string(path).await {
7+
Ok(aliases) => aliases,
8+
Err(r) => {
9+
// we failed to read the file for some reason, but the file does exist
10+
// fallback to generating new aliases on the fly
11+
12+
store.powershell().await.unwrap_or_else(|e| {
13+
format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",)
14+
})
15+
}
16+
}
17+
}
18+
19+
async fn cached_vars(path: PathBuf, store: &VarStore) -> String {
20+
match tokio::fs::read_to_string(path).await {
21+
Ok(vars) => vars,
22+
Err(r) => {
23+
// we failed to read the file for some reason, but the file does exist
24+
// fallback to generating new vars on the fly
25+
26+
store.powershell().await.unwrap_or_else(|e| {
27+
format!("echo 'Atuin: failed to read and generate vars: \n{r}\n{e}'",)
28+
})
29+
}
30+
}
31+
}
32+
33+
/// Return powershell dotfile config
34+
///
35+
/// Do not return an error. We should not prevent the shell from starting.
36+
///
37+
/// In the worst case, Atuin should not function but the shell should start correctly.
38+
///
39+
/// While currently this only returns aliases, it will be extended to also return other synced dotfiles
40+
pub async fn alias_config(store: &AliasStore) -> String {
41+
// First try to read the cached config
42+
let aliases = atuin_common::utils::dotfiles_cache_dir().join("aliases.ps1");
43+
44+
if aliases.exists() {
45+
return cached_aliases(aliases, store).await;
46+
}
47+
48+
if let Err(e) = store.build().await {
49+
return format!("echo 'Atuin: failed to generate aliases: {}'", e);
50+
}
51+
52+
cached_aliases(aliases, store).await
53+
}
54+
55+
pub async fn var_config(store: &VarStore) -> String {
56+
// First try to read the cached config
57+
let vars = atuin_common::utils::dotfiles_cache_dir().join("vars.ps1");
58+
59+
if vars.exists() {
60+
return cached_vars(vars, store).await;
61+
}
62+
63+
if let Err(e) = store.build().await {
64+
return format!("echo 'Atuin: failed to generate vars: {}'", e);
65+
}
66+
67+
cached_vars(vars, store).await
68+
}

Diff for: crates/atuin-dotfiles/src/store.rs

+73-7
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,20 @@ impl AliasStore {
142142

143143
pub async fn posix(&self) -> Result<String> {
144144
let aliases = self.aliases().await?;
145+
Ok(Self::format_posix(&aliases))
146+
}
147+
148+
pub async fn xonsh(&self) -> Result<String> {
149+
let aliases = self.aliases().await?;
150+
Ok(Self::format_xonsh(&aliases))
151+
}
145152

153+
pub async fn powershell(&self) -> Result<String> {
154+
let aliases = self.aliases().await?;
155+
Ok(Self::format_powershell(&aliases))
156+
}
157+
158+
fn format_posix(aliases: &[Alias]) -> String {
146159
let mut config = String::new();
147160

148161
for alias in aliases {
@@ -153,28 +166,50 @@ impl AliasStore {
153166
config.push_str(&format!("alias {}='{}'\n", alias.name, value));
154167
}
155168

156-
Ok(config)
169+
config
157170
}
158171

159-
pub async fn xonsh(&self) -> Result<String> {
160-
let aliases = self.aliases().await?;
161-
172+
fn format_xonsh(aliases: &[Alias]) -> String {
162173
let mut config = String::new();
163174

164175
for alias in aliases {
165176
config.push_str(&format!("aliases['{}'] ='{}'\n", alias.name, alias.value));
166177
}
167178

168-
Ok(config)
179+
config
180+
}
181+
182+
fn format_powershell(aliases: &[Alias]) -> String {
183+
let mut config = String::new();
184+
185+
for alias in aliases {
186+
// Set-Alias doesn't support adding implicit arguments, so use a function.
187+
// See https://github.com/PowerShell/PowerShell/issues/12962
188+
config.push_str(&format!(
189+
"\nfunction {} {{\n {}{} @args\n}}\n",
190+
alias.name,
191+
if alias.value.starts_with(['"', '\'']) {
192+
"& "
193+
} else {
194+
""
195+
},
196+
alias.value
197+
));
198+
}
199+
200+
config
169201
}
170202

171203
pub async fn build(&self) -> Result<()> {
172204
let dir = atuin_common::utils::dotfiles_cache_dir();
173205
tokio::fs::create_dir_all(dir.clone()).await?;
174206

207+
let aliases = self.aliases().await?;
208+
175209
// Build for all supported shells
176-
let posix = self.posix().await?;
177-
let xonsh = self.xonsh().await?;
210+
let posix = Self::format_posix(&aliases);
211+
let xonsh = Self::format_xonsh(&aliases);
212+
let powershell = Self::format_powershell(&aliases);
178213

179214
// All the same contents, maybe optimize in the future or perhaps there will be quirks
180215
// per-shell
@@ -183,11 +218,13 @@ impl AliasStore {
183218
let bash = dir.join("aliases.bash");
184219
let fish = dir.join("aliases.fish");
185220
let xsh = dir.join("aliases.xsh");
221+
let ps1 = dir.join("aliases.ps1");
186222

187223
tokio::fs::write(zsh, &posix).await?;
188224
tokio::fs::write(bash, &posix).await?;
189225
tokio::fs::write(fish, &posix).await?;
190226
tokio::fs::write(xsh, &xonsh).await?;
227+
tokio::fs::write(ps1, &powershell).await?;
191228

192229
Ok(())
193230
}
@@ -389,6 +426,35 @@ mod tests {
389426
"alias gp='git push'
390427
alias k='kubectl'
391428
alias kgap='kubectl get pods --all-namespaces'
429+
"
430+
)
431+
}
432+
433+
#[test]
434+
fn format_powershell() {
435+
let aliases = [
436+
Alias {
437+
name: "gp".to_string(),
438+
value: "git push".to_string(),
439+
},
440+
Alias {
441+
name: "spc".to_string(),
442+
value: "\"path with spaces\" arg".to_string(),
443+
},
444+
];
445+
446+
let result = AliasStore::format_powershell(&aliases);
447+
448+
assert_eq!(
449+
result,
450+
"
451+
function gp {
452+
git push @args
453+
}
454+
455+
function spc {
456+
& \"path with spaces\" arg @args
457+
}
392458
"
393459
)
394460
}

Diff for: crates/atuin-dotfiles/src/store/var.rs

+65-12
Original file line numberDiff line numberDiff line change
@@ -117,31 +117,45 @@ impl VarStore {
117117

118118
pub async fn xonsh(&self) -> Result<String> {
119119
let env = self.vars().await?;
120+
Ok(Self::format_xonsh(&env))
121+
}
122+
123+
pub async fn fish(&self) -> Result<String> {
124+
let env = self.vars().await?;
125+
Ok(Self::format_fish(&env))
126+
}
120127

128+
pub async fn posix(&self) -> Result<String> {
129+
let env = self.vars().await?;
130+
Ok(Self::format_posix(&env))
131+
}
132+
133+
pub async fn powershell(&self) -> Result<String> {
134+
let env = self.vars().await?;
135+
Ok(Self::format_powershell(&env))
136+
}
137+
138+
fn format_xonsh(env: &[Var]) -> String {
121139
let mut config = String::new();
122140

123141
for env in env {
124142
config.push_str(&format!("${}={}\n", env.name, env.value));
125143
}
126144

127-
Ok(config)
145+
config
128146
}
129147

130-
pub async fn fish(&self) -> Result<String> {
131-
let env = self.vars().await?;
132-
148+
fn format_fish(env: &[Var]) -> String {
133149
let mut config = String::new();
134150

135151
for env in env {
136152
config.push_str(&format!("set -gx {} {}\n", env.name, env.value));
137153
}
138154

139-
Ok(config)
155+
config
140156
}
141157

142-
pub async fn posix(&self) -> Result<String> {
143-
let env = self.vars().await?;
144-
158+
fn format_posix(env: &[Var]) -> String {
145159
let mut config = String::new();
146160

147161
for env in env {
@@ -152,17 +166,34 @@ impl VarStore {
152166
}
153167
}
154168

155-
Ok(config)
169+
config
170+
}
171+
172+
fn format_powershell(env: &[Var]) -> String {
173+
let mut config = String::new();
174+
175+
for env in env {
176+
config.push_str(&format!(
177+
"$env:{} = '{}'\n",
178+
env.name,
179+
env.value.replace("'", "''")
180+
));
181+
}
182+
183+
config
156184
}
157185

158186
pub async fn build(&self) -> Result<()> {
159187
let dir = atuin_common::utils::dotfiles_cache_dir();
160188
tokio::fs::create_dir_all(dir.clone()).await?;
161189

190+
let env = self.vars().await?;
191+
162192
// Build for all supported shells
163-
let posix = self.posix().await?;
164-
let xonsh = self.xonsh().await?;
165-
let fsh = self.fish().await?;
193+
let posix = Self::format_posix(&env);
194+
let xonsh = Self::format_xonsh(&env);
195+
let fsh = Self::format_fish(&env);
196+
let powershell = Self::format_powershell(&env);
166197

167198
// All the same contents, maybe optimize in the future or perhaps there will be quirks
168199
// per-shell
@@ -171,11 +202,13 @@ impl VarStore {
171202
let bash = dir.join("vars.bash");
172203
let fish = dir.join("vars.fish");
173204
let xsh = dir.join("vars.xsh");
205+
let ps1 = dir.join("vars.ps1");
174206

175207
tokio::fs::write(zsh, &posix).await?;
176208
tokio::fs::write(bash, &posix).await?;
177209
tokio::fs::write(fish, &fsh).await?;
178210
tokio::fs::write(xsh, &xonsh).await?;
211+
tokio::fs::write(ps1, &powershell).await?;
179212

180213
Ok(())
181214
}
@@ -354,4 +387,24 @@ mod tests {
354387
}
355388
);
356389
}
390+
391+
#[test]
392+
fn format_powershell() {
393+
let env = [
394+
Var {
395+
name: "FOO".to_owned(),
396+
value: "bar 'baz'".to_owned(),
397+
export: true,
398+
},
399+
Var {
400+
name: "TEST".to_owned(),
401+
value: "1".to_owned(),
402+
export: true,
403+
},
404+
];
405+
406+
let result = VarStore::format_powershell(&env);
407+
408+
assert_eq!(result, "$env:FOO = 'bar ''baz'''\n$env:TEST = '1'\n");
409+
}
357410
}

Diff for: crates/atuin/src/command/client/init.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,13 @@ $env.config = (
162162
.await?;
163163
}
164164
Shell::PowerShell => {
165-
powershell::init_static(self.disable_up_arrow, self.disable_ctrl_r);
165+
powershell::init(
166+
alias_store,
167+
var_store,
168+
self.disable_up_arrow,
169+
self.disable_ctrl_r,
170+
)
171+
.await?;
166172
}
167173
}
168174

Diff for: crates/atuin/src/command/client/init/powershell.rs

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use atuin_dotfiles::store::{var::VarStore, AliasStore};
2+
13
pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
24
let base = include_str!("../../../shell/atuin.ps1");
35

@@ -15,6 +17,23 @@ pub fn init_static(disable_up_arrow: bool, disable_ctrl_r: bool) {
1517
);
1618
}
1719

20+
pub async fn init(
21+
aliases: AliasStore,
22+
vars: VarStore,
23+
disable_up_arrow: bool,
24+
disable_ctrl_r: bool,
25+
) -> eyre::Result<()> {
26+
init_static(disable_up_arrow, disable_ctrl_r);
27+
28+
let aliases = atuin_dotfiles::shell::powershell::alias_config(&aliases).await;
29+
let vars = atuin_dotfiles::shell::powershell::var_config(&vars).await;
30+
31+
println!("{aliases}");
32+
println!("{vars}");
33+
34+
Ok(())
35+
}
36+
1837
fn ps_bool(value: bool) -> &'static str {
1938
if value {
2039
"$true"

0 commit comments

Comments
 (0)