Skip to content

Commit d620d93

Browse files
committed
feat(powershell): add dotfiles support
1 parent 3c3b32d commit d620d93

File tree

6 files changed

+270
-20
lines changed

6 files changed

+270
-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

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
use crate::shell::{Alias, Var};
2+
use crate::store::{AliasStore, var::VarStore};
3+
use std::path::PathBuf;
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+
}
69+
70+
pub fn format_alias(alias: &Alias) -> String {
71+
// Set-Alias doesn't support adding implicit arguments, so use a function.
72+
// See https://github.com/PowerShell/PowerShell/issues/12962
73+
74+
let mut result = secure_command(&format!(
75+
"function {} {{\n {}{} @args\n}}",
76+
alias.name,
77+
if alias.value.starts_with(['"', '\'']) {
78+
"& "
79+
} else {
80+
""
81+
},
82+
alias.value
83+
));
84+
85+
// This makes the file layout prettier
86+
result.insert(0, '\n');
87+
result
88+
}
89+
90+
pub fn format_var(var: &Var) -> String {
91+
secure_command(&format!(
92+
"${}{} = '{}'",
93+
if var.export { "env:" } else { "" },
94+
var.name,
95+
var.value.replace("'", "''")
96+
))
97+
}
98+
99+
/// Wraps the given command in an Invoke-Expression to ensure the outer script is not halted
100+
/// if the inner command contains a syntax error.
101+
fn secure_command(command: &str) -> String {
102+
format!(
103+
"Invoke-Expression -ErrorAction Continue -Command '{}'\n",
104+
command.replace("'", "''")
105+
)
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use super::*;
111+
112+
#[test]
113+
fn aliases() {
114+
assert_eq!(
115+
format_alias(&Alias {
116+
name: "gp".to_string(),
117+
value: "git push".to_string(),
118+
}),
119+
"\n".to_string()
120+
+ &secure_command(
121+
"function gp {
122+
git push @args
123+
}"
124+
)
125+
);
126+
127+
assert_eq!(
128+
format_alias(&Alias {
129+
name: "spc".to_string(),
130+
value: "\"path with spaces\" arg".to_string(),
131+
}),
132+
"\n".to_string()
133+
+ &secure_command(
134+
"function spc {
135+
& \"path with spaces\" arg @args
136+
}"
137+
)
138+
);
139+
}
140+
141+
#[test]
142+
fn vars() {
143+
assert_eq!(
144+
format_var(&Var {
145+
name: "FOO".to_owned(),
146+
value: "bar 'baz'".to_owned(),
147+
export: true,
148+
}),
149+
secure_command("$env:FOO = 'bar ''baz'''")
150+
);
151+
152+
assert_eq!(
153+
format_var(&Var {
154+
name: "TEST".to_owned(),
155+
value: "1".to_owned(),
156+
export: false,
157+
}),
158+
secure_command("$TEST = '1'")
159+
);
160+
}
161+
162+
#[test]
163+
fn invoke_expression() {
164+
assert_eq!(
165+
secure_command("echo 'foo'"),
166+
"Invoke-Expression -ErrorAction Continue -Command 'echo ''foo'''\n"
167+
)
168+
}
169+
}

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

+33-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,39 @@ 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+
config.push_str(&crate::shell::powershell::format_alias(alias));
187+
}
188+
189+
config
169190
}
170191

171192
pub async fn build(&self) -> Result<()> {
172193
let dir = atuin_common::utils::dotfiles_cache_dir();
173194
tokio::fs::create_dir_all(dir.clone()).await?;
174195

196+
let aliases = self.aliases().await?;
197+
175198
// Build for all supported shells
176-
let posix = self.posix().await?;
177-
let xonsh = self.xonsh().await?;
199+
let posix = Self::format_posix(&aliases);
200+
let xonsh = Self::format_xonsh(&aliases);
201+
let powershell = Self::format_powershell(&aliases);
178202

179203
// All the same contents, maybe optimize in the future or perhaps there will be quirks
180204
// per-shell
@@ -183,11 +207,13 @@ impl AliasStore {
183207
let bash = dir.join("aliases.bash");
184208
let fish = dir.join("aliases.fish");
185209
let xsh = dir.join("aliases.xsh");
210+
let ps1 = dir.join("aliases.ps1");
186211

187212
tokio::fs::write(zsh, &posix).await?;
188213
tokio::fs::write(bash, &posix).await?;
189214
tokio::fs::write(fish, &posix).await?;
190215
tokio::fs::write(xsh, &xonsh).await?;
216+
tokio::fs::write(ps1, &powershell).await?;
191217

192218
Ok(())
193219
}

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

+41-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+
}
120122

123+
pub async fn fish(&self) -> Result<String> {
124+
let env = self.vars().await?;
125+
Ok(Self::format_fish(&env))
126+
}
127+
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,30 @@ 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 var in env {
176+
config.push_str(&crate::shell::powershell::format_var(var));
177+
}
178+
179+
config
156180
}
157181

158182
pub async fn build(&self) -> Result<()> {
159183
let dir = atuin_common::utils::dotfiles_cache_dir();
160184
tokio::fs::create_dir_all(dir.clone()).await?;
161185

186+
let env = self.vars().await?;
187+
162188
// Build for all supported shells
163-
let posix = self.posix().await?;
164-
let xonsh = self.xonsh().await?;
165-
let fsh = self.fish().await?;
189+
let posix = Self::format_posix(&env);
190+
let xonsh = Self::format_xonsh(&env);
191+
let fsh = Self::format_fish(&env);
192+
let powershell = Self::format_powershell(&env);
166193

167194
// All the same contents, maybe optimize in the future or perhaps there will be quirks
168195
// per-shell
@@ -171,11 +198,13 @@ impl VarStore {
171198
let bash = dir.join("vars.bash");
172199
let fish = dir.join("vars.fish");
173200
let xsh = dir.join("vars.xsh");
201+
let ps1 = dir.join("vars.ps1");
174202

175203
tokio::fs::write(zsh, &posix).await?;
176204
tokio::fs::write(bash, &posix).await?;
177205
tokio::fs::write(fish, &fsh).await?;
178206
tokio::fs::write(xsh, &xonsh).await?;
207+
tokio::fs::write(ps1, &powershell).await?;
179208

180209
Ok(())
181210
}

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

0 commit comments

Comments
 (0)