Skip to content

Commit 6a834c8

Browse files
Merge pull request #1349 from SierraSoftworks/feat/temp
feat: Add a `gt temp` command to create a temporary directory
2 parents fabaff6 + 7f0ac10 commit 6a834c8

File tree

11 files changed

+352
-35
lines changed

11 files changed

+352
-35
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ serde = { version = "1.0", features = ["derive", "rc"] }
3737
serde_json = "1.0"
3838
serde_yaml = "0.9"
3939
shell-words = "1.1"
40+
tempfile = "3.16"
4041
tokio = { version = "1.43", features = [
4142
"rt",
4243
"time",
@@ -50,7 +51,6 @@ trust-dns-resolver = { version = "0.23", features = ["tokio-runtime"] }
5051

5152
[dev-dependencies]
5253
mockall = "0.13.1"
53-
tempfile = "3.16"
5454
sentry = { version = "0.36", default-features = false, features = [
5555
"reqwest",
5656
"rustls",

docs/.vuepress/components/Releases.vue

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,17 @@
44

55
<div class="release-platforms">
66
<div>Select your Platform:</div>
7-
<button
8-
class="release-button release-platform"
9-
:class="{ active: platform === selectedPlatform }"
10-
v-for="(name, platform) in platforms"
11-
:key="platform"
12-
v-on:click="selectedPlatform = platform"
13-
>
7+
<button class="release-button release-platform" :class="{ active: platform === selectedPlatform }"
8+
v-for="(name, platform) in platforms" :key="platform" v-on:click="selectedPlatform = platform">
149
{{ name }}
1510
</button>
1611
</div>
1712

1813
<div class="release-list" v-if="selectedPlatform">
19-
<div
20-
class="release"
21-
v-for="release in applicableReleases"
22-
:key="release.id"
23-
>
14+
<div class="release" v-for="release in applicableReleases" :key="release.id">
2415
<h4 class="release__name">
25-
<a
26-
class="release-button"
27-
:href="
28-
getReleaseAsset(release, selectedPlatform).browser_download_url
29-
"
30-
target="_blank"
31-
>Download</a
32-
>
16+
<a class="release-button no-external-link-icon" :href="getReleaseAsset(release, selectedPlatform).browser_download_url
17+
" target="_blank">Download</a>
3318

3419
{{ release.name }}
3520
<Badge v-if="release.prerelease" text="Early Access" type="warning" />
@@ -137,6 +122,11 @@ export default defineComponent({
137122
margin: 20px;
138123
}
139124
125+
.release__name {
126+
margin-top: 1em;
127+
padding-top: 0;
128+
}
129+
140130
.release__notes {
141131
white-space: pre-wrap;
142132
word-break: break-word;
@@ -152,8 +142,8 @@ export default defineComponent({
152142
}
153143
154144
.release-platform.active {
155-
background: var(--c-brand);
156-
color: var(--c-bg);
145+
background: var(--vp-c-accent-bg);
146+
color: var(--vp-c-accent-text);
157147
}
158148
159149
.release-assets {
@@ -173,17 +163,22 @@ export default defineComponent({
173163
.release-button {
174164
background: none;
175165
border-radius: 5px;
176-
color: var(--c-brand);
177-
border: 1px solid var(--c-brand);
166+
background: var();
167+
color: var(--vp-c-accent-bg);
168+
border: 1px solid var(--vp-c-accent-bg);
178169
font-size: 80%;
179170
padding: 7px;
180171
margin: 5px;
181172
cursor: pointer;
182173
}
183174
175+
a.release-button {
176+
text-decoration: none;
177+
}
178+
184179
.release-button:hover,
185180
.release-button:focus {
186-
background: var(--c-brand-light);
187-
color: var(--c-bg);
181+
background: var(--vp-c-accent-hover);
182+
color: var(--vp-c-accent-text);
188183
}
189-
</style>
184+
</style>

docs/commands/scratch.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,39 @@ gt s code
5050
# Open a specially named scratchpad folder
5151
gt s 2021w10-super-important
5252
```
53-
53+
5454
::: tip
5555
You don't need to use our naming scheme if you don't want to, just run `gt s something` and
5656
we'll create a `something` folder for you with no complaints. *This can be useful if you
5757
have an important project which you don't want to lose track of.*
5858
:::
59+
60+
## temp <Badge text="v3.6.0+" />
61+
The `gt temp` command is a shortcut for opening a temporary scratchpad which is automatically
62+
deleted when you close the launched application. This is useful when you want to quickly test
63+
something without leaving a mess behind.
64+
65+
::: tip
66+
The `gt temp` command creates a temporary folder in your current user's temporary directory
67+
and launches the specified application in that folder. When the application exits, the folder
68+
will be automatically deleted.
69+
:::
70+
71+
#### Example
72+
```powershell
73+
# Open a temporary scratchpad in your default app
74+
gt t
75+
76+
# Open a temporary scratchpad in PowerShell
77+
gt temp pwsh
78+
79+
# Open a temporary scratchpad and don't delete it when you're done
80+
gt temp --keep
81+
```
82+
83+
::: warning
84+
Applications which exit immediately after launching (like VSCode's `code` command) will cause
85+
the temporary folder to be deleted immediately after the application is launched. You can prevent
86+
this by using the `--keep` flag to prevent the folder from being deleted - however you will need
87+
to manually clean it up when you're done (in this case it works much the same as a standard scratchpad).
88+
:::

docs/package-lock.json

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
nativeBuildInputs = with pkgs; [
112112
cargo
113113
rustc
114+
nodejs
114115
] ++ nativeBuildInputs ++ buildInputs;
115116
};
116117
});

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ mod services;
3030
mod setup;
3131
mod shell_init;
3232
mod switch;
33+
mod temp;
3334
mod update;
3435

3536
inventory::collect!(Command);

src/commands/temp.rs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
use super::async_trait;
2+
use super::*;
3+
use crate::core::Target;
4+
use clap::Arg;
5+
use tracing_batteries::prelude::*;
6+
7+
pub struct TempCommand;
8+
crate::command!(TempCommand);
9+
10+
#[async_trait]
11+
impl CommandRunnable for TempCommand {
12+
fn name(&self) -> String {
13+
String::from("temp")
14+
}
15+
16+
fn app(&self) -> clap::Command {
17+
clap::Command::new(self.name())
18+
.version("1.0")
19+
.visible_alias("t")
20+
.about("opens a temporary folder which will be removed when the shell is closed")
21+
.arg(
22+
Arg::new("app")
23+
.help("The name of the application to launch.")
24+
.index(1),
25+
)
26+
.arg(
27+
Arg::new("keep")
28+
.long("keep")
29+
.short('k')
30+
.help("do not remove the temp directory when the app exits.")
31+
.action(clap::ArgAction::SetTrue),
32+
)
33+
}
34+
35+
#[tracing::instrument(name = "gt temp", err, skip(self, core, matches))]
36+
async fn run(&self, core: &Core, matches: &ArgMatches) -> Result<i32, errors::Error> {
37+
let app = if let Some(app) = matches.get_one::<String>("app") {
38+
core.config().get_app(app).ok_or_else(|| errors::user(
39+
"The specified application does not exist.",
40+
"Make sure you have added the application to your config file using 'git-tool config add apps/bash' or similar."))?
41+
} else {
42+
core.config().get_default_app().ok_or_else(|| errors::user(
43+
"No default application available.",
44+
"Make sure that you add an app to your config file using 'git-tool config add apps/bash' or similar."))?
45+
};
46+
47+
let keep = matches.get_one::<bool>("keep").copied().unwrap_or_default();
48+
49+
let temp = core.resolver().get_temp(keep)?;
50+
51+
if keep {
52+
writeln!(core.output(), "temp path: {}", temp.get_path().display())?;
53+
}
54+
55+
let status = core.launcher().run(app, &temp).await?;
56+
temp.close()?;
57+
58+
return Ok(status);
59+
}
60+
61+
#[tracing::instrument(name = "gt complete -- gt temp", skip(self, core, completer, _matches))]
62+
async fn complete(&self, core: &Core, completer: &Completer, _matches: &ArgMatches) {
63+
completer.offer_many(core.config().get_apps().map(|a| a.get_name()));
64+
completer.offer("--keep");
65+
}
66+
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use super::*;
71+
use crate::core::*;
72+
use std::sync::{Arc, RwLock};
73+
74+
#[tokio::test]
75+
async fn run_no_args() {
76+
let cmd = TempCommand {};
77+
78+
let args = cmd.app().get_matches_from(vec!["temp"]);
79+
80+
let temp = tempfile::tempdir().unwrap();
81+
let cfg = Config::from_str(&format!(
82+
"
83+
directory: {}=
84+
85+
apps:
86+
- name: test-app
87+
command: test
88+
args:
89+
- '{{ .Target.Name }}'
90+
",
91+
temp.path().display(),
92+
))
93+
.unwrap();
94+
95+
let temp_path = Arc::new(RwLock::new(None));
96+
let core = Core::builder()
97+
.with_config(cfg)
98+
.with_mock_resolver(|mock| {
99+
let temp_path = temp_path.clone();
100+
mock.expect_get_temp().returning(move |keep| {
101+
let temp = TempTarget::new(keep).unwrap();
102+
*temp_path.write().unwrap() = Some(temp.get_path().to_owned());
103+
Ok(temp)
104+
});
105+
})
106+
.with_mock_launcher(|mock| {
107+
let temp_path = temp_path.clone();
108+
mock.expect_run()
109+
.once()
110+
.withf(move |app, target| {
111+
app.get_name() == "test-app"
112+
&& target.get_path() == *temp_path.read().unwrap().as_ref().unwrap()
113+
})
114+
.returning(|_, _| Box::pin(async { Ok(5) }));
115+
})
116+
.build();
117+
118+
match cmd.run(&core, &args).await {
119+
Ok(status) => {
120+
assert_eq!(status, 5, "it should forward the status code from the app");
121+
}
122+
Err(err) => panic!("{}", err.message()),
123+
}
124+
125+
assert!(
126+
!temp_path.read().unwrap().as_ref().unwrap().exists(),
127+
"the temp dir should be removed when the app exits"
128+
);
129+
}
130+
131+
#[tokio::test]
132+
async fn run_only_app() {
133+
let cmd = TempCommand {};
134+
135+
let args = cmd.app().get_matches_from(vec!["temp", "test-app"]);
136+
137+
let temp = tempfile::tempdir().unwrap();
138+
let cfg = Config::from_str(&format!(
139+
"
140+
directory: {}
141+
142+
apps:
143+
- name: test-app
144+
command: test
145+
args:
146+
- '{{ .Target.Name }}'
147+
",
148+
temp.path().display(),
149+
))
150+
.unwrap();
151+
152+
let temp_path = Arc::new(RwLock::new(None));
153+
let core = Core::builder()
154+
.with_config(cfg)
155+
.with_mock_resolver(|mock| {
156+
let temp_path = temp_path.clone();
157+
mock.expect_get_temp().returning(move |keep| {
158+
let temp = TempTarget::new(keep).unwrap();
159+
*temp_path.write().unwrap() = Some(temp.get_path().to_owned());
160+
Ok(temp)
161+
});
162+
})
163+
.with_mock_launcher(|mock| {
164+
let temp_path = temp_path.clone();
165+
mock.expect_run()
166+
.once()
167+
.withf(move |app, target| {
168+
app.get_name() == "test-app"
169+
&& target.get_path() == *temp_path.read().unwrap().as_ref().unwrap()
170+
})
171+
.returning(|_, _| Box::pin(async { Ok(0) }));
172+
})
173+
.build();
174+
175+
match cmd.run(&core, &args).await {
176+
Ok(_) => {}
177+
Err(err) => panic!("{}", err.message()),
178+
}
179+
180+
assert!(
181+
!temp_path.read().unwrap().as_ref().unwrap().exists(),
182+
"the temp dir should be removed when the app exits"
183+
);
184+
}
185+
186+
#[tokio::test]
187+
async fn run_unknown_app() {
188+
let cmd = TempCommand {};
189+
190+
let args = cmd.app().get_matches_from(vec!["temp", "unknown-app"]);
191+
192+
let temp = tempfile::tempdir().unwrap();
193+
let cfg = Config::from_str(&format!(
194+
"
195+
directory: {}
196+
197+
apps:
198+
- name: test-app
199+
command: test
200+
args:
201+
- '{{ .Target.Name }}'
202+
",
203+
temp.path().display(),
204+
))
205+
.unwrap();
206+
207+
let core = Core::builder()
208+
.with_config(cfg)
209+
.with_mock_resolver(|mock| {
210+
mock.expect_get_temp()
211+
.returning(|keep| Ok(TempTarget::new(keep).unwrap()));
212+
})
213+
.with_mock_launcher(|mock| {
214+
mock.expect_run().never();
215+
})
216+
.build();
217+
218+
cmd.run(&core, &args).await.unwrap_or_default();
219+
}
220+
}

0 commit comments

Comments
 (0)