Compare commits

...

11 Commits

Author SHA1 Message Date
862c68c846 update todo 2025-01-04 11:02:58 -05:00
ac62171467 upgrade to tauri 2.2.0 2025-01-04 10:50:04 -05:00
2080fb897b fix autolaunch name in dev/prod 2025-01-03 16:43:45 -05:00
bf62054c2b make windows work again 2025-01-03 07:52:10 -05:00
cd4c613758 improve start-minimized and start-on-login behavior
Previously, when Creddy was configured to start minimized, it would always start minimized, regardless of how it was launched. Really, though, when you use this setting what you probably want is for it to start minimized only when it's being launched automatically, i.e. on login. This update changes its behavior so that it will only start minimized when auto-launching.

Additionally, if Creddy detects on startup that its start-on-login configuration doesn't match the system, it will modify its own settings to match the system (unless it's the very first launch, of course.) That way if you disable Creddy's start-on-login behavior from your system dialog, it will respect your change.
2024-12-30 21:09:45 -05:00
efbf6c687c add test to ensure that client and server agree on socket address 2024-12-28 07:36:38 -05:00
ee495478ff start working on test for server address 2024-12-28 07:24:43 -05:00
4c18de8b7a fix docker credential helper when credentials are not found 2024-12-28 06:59:09 -05:00
0cfa9fc07a correct server socket differentiation 2024-12-27 15:49:42 -05:00
9e9bc2b0ae separate dev and production instances and add visual indicators of dev mode 2024-12-27 08:17:49 -05:00
07bf98e522 bump version to 0.6.0 2024-11-25 14:58:53 -05:00
32 changed files with 3951 additions and 2269 deletions

View File

@ -11,7 +11,8 @@
* Logging * Logging
* Icon * Icon
* Auto-updates * Auto-updates
* SSH key handling * ~~SSH key handling~~
* ~~Docker credential helper~~
* Encrypted sync server * Encrypted sync server
## Maybe ## Maybe

92
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "creddy", "name": "creddy",
"version": "0.4.9", "version": "0.6.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "creddy", "name": "creddy",
"version": "0.4.9", "version": "0.6.4",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.0.0-beta.13", "@tauri-apps/api": "^2.0.0-beta.13",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.5", "@tauri-apps/plugin-dialog": "^2.0.0-beta.5",
@ -15,7 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tauri-apps/cli": "^2.0.0-beta.20", "@tauri-apps/cli": "^2.2.1",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"postcss": "^8.4.16", "postcss": "^8.4.16",
"svelte": "^3.49.0", "svelte": "^3.49.0",
@ -213,9 +213,9 @@
} }
}, },
"node_modules/@tauri-apps/cli": { "node_modules/@tauri-apps/cli": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.2.1.tgz",
"integrity": "sha512-707q9uIc2oNrYHd2dtMvxTrpZXVpart5EIktnRymNOpphkLlB6WUBjHD+ga45WqTU6cNGKbYvkKqTNfshNul9Q==", "integrity": "sha512-oLWX/2tW0v8cBaShI9/bt5RsquCLK7ZCwhPXXnf55oil8/GrNtLzW9/67iyydcnxiYYU5jYMpo3uXptknOSdpA==",
"dev": true, "dev": true,
"bin": { "bin": {
"tauri": "tauri.js" "tauri": "tauri.js"
@ -228,22 +228,22 @@
"url": "https://opencollective.com/tauri" "url": "https://opencollective.com/tauri"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.0.0-beta.20", "@tauri-apps/cli-darwin-arm64": "2.2.1",
"@tauri-apps/cli-darwin-x64": "2.0.0-beta.20", "@tauri-apps/cli-darwin-x64": "2.2.1",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-beta.20", "@tauri-apps/cli-linux-arm-gnueabihf": "2.2.1",
"@tauri-apps/cli-linux-arm64-gnu": "2.0.0-beta.20", "@tauri-apps/cli-linux-arm64-gnu": "2.2.1",
"@tauri-apps/cli-linux-arm64-musl": "2.0.0-beta.20", "@tauri-apps/cli-linux-arm64-musl": "2.2.1",
"@tauri-apps/cli-linux-x64-gnu": "2.0.0-beta.20", "@tauri-apps/cli-linux-x64-gnu": "2.2.1",
"@tauri-apps/cli-linux-x64-musl": "2.0.0-beta.20", "@tauri-apps/cli-linux-x64-musl": "2.2.1",
"@tauri-apps/cli-win32-arm64-msvc": "2.0.0-beta.20", "@tauri-apps/cli-win32-arm64-msvc": "2.2.1",
"@tauri-apps/cli-win32-ia32-msvc": "2.0.0-beta.20", "@tauri-apps/cli-win32-ia32-msvc": "2.2.1",
"@tauri-apps/cli-win32-x64-msvc": "2.0.0-beta.20" "@tauri-apps/cli-win32-x64-msvc": "2.2.1"
} }
}, },
"node_modules/@tauri-apps/cli-darwin-arm64": { "node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.2.1.tgz",
"integrity": "sha512-oCJOCib7GuYkwkBXx+ekamR8NZZU+2i3MLP+DHpDxK5gS2uhCE+CBkamJkNt6y1x6xdVnwyqZOm5RvN4SRtyIA==", "integrity": "sha512-658OPWObcEA7x/Pe/fAXfyJtC5SdcpD2Q9ZSVKoLBovPzfU6Ug2mCaQmH1L5iA7Zb7a26ctzkaz3Sh3dMeGcJw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -257,9 +257,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-darwin-x64": { "node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.2.1.tgz",
"integrity": "sha512-lC5QSnRExedYN4Ds6ZlSvC2PxP8qfIYBJQ5ktf+PJI5gQALdNeVtd6YnTG1ODCEklfLq9WKkGwp7JdALTU5wDA==", "integrity": "sha512-3g11km4caJa6StvETI5GIynniNC/e9AWpUy+lWQRfQBdelRrEGoEDw949SihxqKHAoP2E9cm7z5DUsiRiT/Yaw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -273,9 +273,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.2.1.tgz",
"integrity": "sha512-nZCeBMHHye5DLOJV5k2w658hnCS+LYaOZ8y/G9l3ei+g0L/HBjlSy6r4simsAT5TG8+l3oCZzLBngfTMdDS/YA==", "integrity": "sha512-Ldbw3Y56TAfpsGRuWJnkdl0TV0NHhtP3bGyjh2lJACofkHMCOtsLHOx4/HP2hFnn7DcSLWHUayyPlj2rAikKkA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -289,9 +289,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm64-gnu": { "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.2.1.tgz",
"integrity": "sha512-B79ISVLPVBgwnCchVqwTKU+vxnFYqxKomcR4rmsvxfs0NVtT5QuNzE1k4NUQnw3966yjwhYR3mnHsSJQSB4Eyw==", "integrity": "sha512-ay3NwilDR95RyvK/AIdivuULcbpGgrUISNLDOfTKEvKMMnRWkMV4gzY3hifQ8H7CDonGhqMl2PjP+WvDQpXUig==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -305,9 +305,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm64-musl": { "node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.1.tgz",
"integrity": "sha512-ojIkv/1uZHhcrgfIN8xgn4BBeo/Xg+bnV0wer6lD78zyxkUMWeEZ+u3mae1ejCJNhhaZOxNaUQ67MvDOiGyr5Q==", "integrity": "sha512-d2zK4Qb9DZlNjNB8Fda0yxOlg6sk6GZGhO5dVnie5VYJMt4lDct2LZljg4boUb5t1pk6sfAPB9356G7R8l4qCQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -321,9 +321,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-x64-gnu": { "node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.2.1.tgz",
"integrity": "sha512-xBy1FNbHKlc7T6pOmFQQPECxJaI5A9QWX7Kb9N64cNVusoOGlvc3xHYkXMS4PTr7xXOT0yiE1Ww2OwDRJ3lYsg==", "integrity": "sha512-P0Zm3nmRbBS/KIxSrzul2ieZEwtTdU4bjsB9pOIk+oPF15HXnrLLbVBeMofNjXOWsIxTJw2tIt/XPD8Jt9jSEg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -337,9 +337,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-x64-musl": { "node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.1.tgz",
"integrity": "sha512-+O6zq5jmtUxA1FUAAwF2ywPysy4NRo2Y6G+ESZDkY9XosRwdt5OUjqAsYktZA3AxDMZVei8r9buwTqUwi9ny/g==", "integrity": "sha512-AwYuKTpPGdR0BJMDdJsjGm8vfVDBpXYRDJ+1B/FlIMTikAx4A/wSODxphjf6Ls9uOC5F3To0XlfqskBkTq0WKw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -353,9 +353,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-arm64-msvc": { "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.2.1.tgz",
"integrity": "sha512-RswgMbWyOQcv53CHvIuiuhAh4kKDqaGyZfWD4VlxqX/XhkoF5gsNgr0MxzrY7pmoL+89oVI+fiGVJz4nOQE5vA==", "integrity": "sha512-t1Pv+Og5O+Cp0uYHFzSWEl+hssr1bKJjgWg05ElTpwYMb4xKA5bh1BTGN5orGqKs0e2+D+EPsOqVfM8KuUWR4Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -369,9 +369,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-ia32-msvc": { "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.2.1.tgz",
"integrity": "sha512-5lgWmDVXhX3SBGbiv5SduM1yajiRnUEJClWhSdRrEEJeXdsxpCsBEhxYnUnDCEzPKxLLn5fdBv3VrVctJ03csQ==", "integrity": "sha512-erY+Spho6hBJgNzHKbA3JFxMztlHAikCiF/OYhk9fy6MbU5KpYHPrAC+Jhj2tcDy/xevWw/6KVNvLmk9PhLcXQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -385,9 +385,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-x64-msvc": { "node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.0.0-beta.20", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.20.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.2.1.tgz",
"integrity": "sha512-SuSiiVQTQPSzWlsxQp/NMzWbzDS9TdVDOw7CCfgiG5wnT2GsxzrcIAVN6i7ILsVFLxrjr0bIgPldSJcdcH84Yw==", "integrity": "sha512-GIdUtdje1CvCn0/Sh3VwPWaFKmD1C0edJUMueGwkRFHmF6HfatXPVhW5FySP+EEO2+rVym1qJkODstJrunraWA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],

View File

@ -1,6 +1,6 @@
{ {
"name": "creddy", "name": "creddy",
"version": "0.5.4", "version": "0.6.5",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@ -9,7 +9,7 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tauri-apps/cli": "^2.0.0-beta.20", "@tauri-apps/cli": "^2.2.1",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"postcss": "^8.4.16", "postcss": "^8.4.16",
"svelte": "^3.49.0", "svelte": "^3.49.0",

1533
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "creddy" name = "creddy"
version = "0.5.4" version = "0.6.5"
description = "A friendly AWS credentials manager" description = "A friendly AWS credentials manager"
authors = ["Joseph Montanaro"] authors = ["Joseph Montanaro"]
license = "" license = ""
@ -22,15 +22,16 @@ dirs = "5.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tokio = { version = ">=1.19", features = ["full"] } tokio = { version = ">=1.19", features = ["full"] }
windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] } tauri-build = { version = "2.0.4", features = [] }
[dependencies] [dependencies]
creddy_cli = { path = "./creddy_cli" } creddy_cli = { path = "./creddy_cli" }
tauri = { version = "2.0.0-beta", features = ["tray-icon"] } tauri = { version = "2.2.0", features = ["tray-icon", "test"] }
sodiumoxide = "0.2.7" sodiumoxide = "0.2.7"
sysinfo = "0.26.8" sysinfo = "0.26.8"
aws-config = "1.5.3" aws-config = "1.5.3"
@ -47,12 +48,10 @@ is-terminal = "0.4.7"
argon2 = { version = "0.5.0", features = ["std"] } argon2 = { version = "0.5.0", features = ["std"] }
chacha20poly1305 = { version = "0.10.1", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] }
which = "4.4.0" which = "4.4.0"
windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] }
time = "0.3.31" time = "0.3.31"
tauri-plugin-single-instance = "2.0.0-beta.9" tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-global-shortcut = "2.0.0-beta.6" tauri-plugin-os = "2.2.0"
tauri-plugin-os = "2.0.0-beta.6" tauri-plugin-dialog = "2.2.0"
tauri-plugin-dialog = "2.0.0-beta.9"
rfd = "0.13.0" rfd = "0.13.0"
ssh-agent-lib = "0.4.0" ssh-agent-lib = "0.4.0"
ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] } ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] }
@ -64,11 +63,14 @@ sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] }
tokio = { workspace = true } tokio = { workspace = true }
tokio-util = { version = "0.7.11", features = ["codec"] } tokio-util = { version = "0.7.11", features = ["codec"] }
futures = "0.3.30" futures = "0.3.30"
openssl = "0.10.64" # openssl = { version = "0.10.64", features = ["vendored"] }
rsa = "0.9.6" rsa = "0.9.6"
sha2 = "0.10.8" sha2 = "0.10.8"
ssh-encoding = "0.2.0" ssh-encoding = "0.2.0"
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL

View File

@ -6,13 +6,13 @@
"main" "main"
], ],
"permissions": [ "permissions": [
"path:default", "core:path:default",
"event:default", "core:event:default",
"window:default", "core:window:default",
"app:default", "core:app:default",
"resources:default", "core:resources:default",
"menu:default", "core:menu:default",
"tray:default", "core:tray:default",
"os:allow-os-type", "os:allow-os-type",
"dialog:allow-open" "dialog:allow-open"
] ]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "creddy_cli" name = "creddy_cli"
version = "0.5.4" version = "0.6.5"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -10,3 +10,6 @@ dirs = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }

View File

@ -29,11 +29,20 @@ pub fn docker_get(global_args: GlobalArgs) -> anyhow::Result<()> {
server_url: server_url.trim().to_owned() server_url: server_url.trim().to_owned()
}; };
match super::make_request(global_args.server_addr, &req)?? { let server_resp = super::make_request(global_args.server_addr, &req)?;
CliResponse::Credential(CliCredential::Docker(d)) => { match server_resp {
Ok(CliResponse::Credential(CliCredential::Docker(d))) => {
println!("{}", serde_json::to_string(&d)?); println!("{}", serde_json::to_string(&d)?);
}, },
r => bail!("Unexpected response from server: {r}"), Err(e) if e.code == "NoCredentials" => {
// To indicate credentials are not found, a credential helper *must* print
// this message to stdout, then exit 1. Any other message/status will cause
// some builds to fail. This is, of course, not documented anywhere.
println!("credentials not found in native keychain");
std::process::exit(1);
},
Err(e) => Err(e)?,
Ok(r) => bail!("Unexpected response from server: {r}"),
} }
Ok(()) Ok(())
} }

View File

@ -2,8 +2,6 @@ use std::path::PathBuf;
use std::process::Command as ChildCommand; use std::process::Command as ChildCommand;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
#[cfg(windows)]
use std::time::Duration;
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use clap::{ use clap::{
@ -65,7 +63,7 @@ pub struct GlobalArgs {
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum Action { pub enum Action {
/// Launch Creddy /// Launch Creddy
Run, Run(RunArgs),
/// Request credentials from Creddy and output to stdout /// Request credentials from Creddy and output to stdout
Get(GetArgs), Get(GetArgs),
/// Inject credentials into the environment of another command /// Inject credentials into the environment of another command
@ -78,6 +76,14 @@ pub enum Action {
} }
#[derive(Debug, Args)]
pub struct RunArgs {
/// Minimize to system tray on launch
#[arg(long, default_value_t = false)]
pub minimized: bool,
}
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct GetArgs { pub struct GetArgs {
/// If unspecified, use default credentials /// If unspecified, use default credentials
@ -102,7 +108,7 @@ pub struct ExecArgs {
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct InvokeArgs { pub struct InvokeArgs {
#[arg(value_name = "ACTION", value_enum)] #[arg(value_name = "ACTION", value_enum)]
shortcut_action: ShortcutAction, pub shortcut_action: ShortcutAction,
} }
@ -176,17 +182,10 @@ pub fn exec(args: ExecArgs, global: GlobalArgs) -> anyhow::Result<()> {
#[cfg(windows)] #[cfg(windows)]
{ {
let mut child = match cmd.spawn() { let mut child = cmd.spawn()
Ok(c) => c, .with_context(|| format!("Failed to execute command: {}", args.command.join(" ")))?;
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let name: OsString = cmd_name.into();
return Err(ExecError::NotFound(name).into());
}
Err(e) => return Err(ExecError::ExecutionFailed(e).into()),
};
let status = child.wait() let status = child.wait()
.map_err(|e| ExecError::ExecutionFailed(e))?; .with_context(|| format!("Failed to execute command: {}", args.command.join(" ")))?;
std::process::exit(status.code().unwrap_or(1)); std::process::exit(status.code().unwrap_or(1));
}; };
} }

View File

@ -1,19 +1,26 @@
mod cli; mod cli;
pub use cli::{ pub use cli::{
Cli,
Action, Action,
Cli,
docker_credential_helper,
exec, exec,
get, get,
GlobalArgs,
RunArgs,
invoke_shortcut, invoke_shortcut,
docker_credential_helper,
}; };
pub(crate) use platform::connect; pub use platform::{connect, server_addr};
pub use platform::server_addr;
pub mod proto; pub mod proto;
pub fn show_window(global_args: GlobalArgs) -> anyhow::Result<()> {
let invoke = cli::InvokeArgs { shortcut_action: proto::ShortcutAction::ShowWindow };
cli::invoke_shortcut(invoke, global_args)
}
#[cfg(unix)] #[cfg(unix)]
mod platform { mod platform {
use std::path::PathBuf; use std::path::PathBuf;
@ -27,7 +34,12 @@ mod platform {
pub fn server_addr(sock_name: &str) -> PathBuf { pub fn server_addr(sock_name: &str) -> PathBuf {
let mut path = dirs::runtime_dir() let mut path = dirs::runtime_dir()
.unwrap_or_else(|| PathBuf::from("/tmp")); .unwrap_or_else(|| PathBuf::from("/tmp"));
path.push(format!("{sock_name}.sock")); if cfg!(debug_assertions) {
path.push(format!("{sock_name}.dev.sock"))
}
else {
path.push(format!("{sock_name}.sock"));
}
path path
} }
} }
@ -35,7 +47,31 @@ mod platform {
#[cfg(windows)] #[cfg(windows)]
mod platform { mod platform {
pub fn server_addr(sock_name: &str) -> String { use std::path::PathBuf;
format!(r"\\.\pipe\{sock_name}") use std::time::Duration;
use tokio::net::windows::named_pipe::{NamedPipeClient, ClientOptions};
use windows::Win32::Foundation::ERROR_PIPE_BUSY;
pub async fn connect(addr: Option<PathBuf>) -> std::io::Result<NamedPipeClient> {
let opts = ClientOptions::new();
let pipe_name = addr.unwrap_or_else(|| server_addr("creddy-server"));
loop {
match opts.open(&pipe_name) {
Ok(client) => return Ok(client),
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => {
tokio::time::sleep(Duration::from_millis(50)).await;
},
Err(e) => return Err(e),
}
}
}
pub fn server_addr(sock_name: &str) -> PathBuf {
if cfg!(debug_assertions) {
format!(r"\\.\pipe\{sock_name}.dev").into()
}
else {
format!(r"\\.\pipe\{sock_name}").into()
}
} }
} }

View File

@ -1,13 +1,17 @@
use std::env; use std::env;
use std::process::{self, Command}; use std::process::{self, Command};
use creddy_cli::{Action, Cli}; use creddy_cli::{
Action,
Cli,
RunArgs,
};
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let res = match cli.action { let res = match cli.action {
None | Some(Action::Run)=> launch_gui(), None => launch_gui(RunArgs { minimized: false }),
Some(Action::Run(run_args)) => launch_gui(run_args),
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args), Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),
Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args), Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args),
Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args), Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args),
@ -21,7 +25,7 @@ fn main() {
} }
fn launch_gui() -> anyhow::Result<()> { fn launch_gui(run_args: RunArgs) -> anyhow::Result<()> {
let mut path = env::current_exe()?; let mut path = env::current_exe()?;
path.pop(); // bin dir path.pop(); // bin dir
@ -31,6 +35,10 @@ fn launch_gui() -> anyhow::Result<()> {
path.push("creddy.exe"); // exe in main install dir (aka gui exe) path.push("creddy.exe"); // exe in main install dir (aka gui exe)
Command::new(path).spawn()?; let mut cmd = Command::new(path);
if run_args.minimized {
cmd.arg("--minimized");
}
cmd.spawn()?;
Ok(()) Ok(())
} }

View File

@ -99,8 +99,8 @@ pub struct DockerCredential {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ServerError { pub struct ServerError {
code: String, pub code: String,
msg: String, pub msg: String,
} }
impl Display for ServerError { impl Display for ServerError {

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","os:allow-os-type","dialog:allow-open"]}} {"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","os:allow-os-type","dialog:allow-open"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ use tauri::{
RunEvent, RunEvent,
WindowEvent, WindowEvent,
}; };
use tauri::menu::MenuItem; use creddy_cli::{GlobalArgs, RunArgs};
use crate::{ use crate::{
config::{self, AppConfig}, config::{self, AppConfig},
@ -32,12 +32,13 @@ use crate::{
pub static APP: OnceCell<AppHandle> = OnceCell::new(); pub static APP: OnceCell<AppHandle> = OnceCell::new();
pub fn run() -> tauri::Result<()> { pub fn run(run_args: RunArgs, global_args: GlobalArgs) -> tauri::Result<()> {
if let Ok(_) = creddy_cli::show_window(global_args) {
// app is already running, so terminate
return Ok(());
}
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
show_main_window(app)
.error_popup("Failed to show main window")
}))
.plugin(tauri_plugin_global_shortcut::Builder::default().build()) .plugin(tauri_plugin_global_shortcut::Builder::default().build())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
@ -58,9 +59,10 @@ pub fn run() -> tauri::Result<()> {
ipc::save_config, ipc::save_config,
ipc::launch_terminal, ipc::launch_terminal,
ipc::get_setup_errors, ipc::get_setup_errors,
ipc::get_devmode,
ipc::exit, ipc::exit,
]) ])
.setup(|app| rt::block_on(setup(app))) .setup(|app| rt::block_on(setup(app, run_args)))
.build(tauri::generate_context!())? .build(tauri::generate_context!())?
.run(|app, run_event| { .run(|app, run_event| {
if let RunEvent::WindowEvent { event, .. } = run_event { if let RunEvent::WindowEvent { event, .. } = run_event {
@ -86,11 +88,11 @@ pub async fn connect_db() -> Result<SqlitePool, SetupError> {
} }
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> { async fn setup(app: &mut App, run_args: RunArgs) -> Result<(), Box<dyn Error>> {
APP.set(app.handle().clone()).unwrap(); APP.set(app.handle().clone()).unwrap();
tray::setup(app)?; tray::setup(app)?;
// get_or_create_db_path doesn't create the actual db file, just the directory // get_or_create_db_path doesn't create the actual db file, just the directory
let is_first_launch = !config::get_or_create_db_path()?.exists(); let is_first_launch = !config::get_or_create_db_path()?.try_exists()?;
let pool = connect_db().await?; let pool = connect_db().await?;
let mut setup_errors: Vec<String> = vec![]; let mut setup_errors: Vec<String> = vec![];
@ -109,10 +111,16 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
creddy_server::serve(app.handle().clone())?; creddy_server::serve(app.handle().clone())?;
agent::serve(app.handle().clone())?; agent::serve(app.handle().clone())?;
config::set_auto_launch(conf.start_on_login)?; // if this is the first launch, setup system with default auto-launch settings
if let Err(_e) = config::set_auto_launch(conf.start_on_login) { if is_first_launch {
setup_errors.push("Error: Failed to manage autolaunch.".into()); if let Err(e) = conf.set_auto_launch() {
setup_errors.push(format!("Failed to manage autolaunch: {e}"));
}
} }
// otherwise, treat the system as the source of truth and ensure ours matches
else {
conf.match_auto_launch(&pool).await?;
};
// if hotkeys fail to register, disable them so that this error doesn't have to keep showing up // if hotkeys fail to register, disable them so that this error doesn't have to keep showing up
if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) { if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) {
@ -125,7 +133,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
.map(|names| names.split(':').any(|n| n == "GNOME")) .map(|names| names.split(':').any(|n| n == "GNOME"))
.unwrap_or(false); .unwrap_or(false);
if !conf.start_minimized || is_first_launch { if !run_args.minimized {
show_main_window(&app.handle())?; show_main_window(&app.handle())?;
} }
@ -158,8 +166,8 @@ fn start_auto_locker(app: AppHandle) {
pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> { pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> {
let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?;
w.show()?; w.show()?;
let show_hide = app.state::<MenuItem<tauri::Wry>>(); let menu = app.state::<tray::MenuItems>();
show_hide.set_text("Hide")?; menu.after_show()?;
Ok(()) Ok(())
} }
@ -167,8 +175,8 @@ pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> {
pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> { pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> {
let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?;
w.hide()?; w.hide()?;
let show_hide = app.state::<MenuItem<tauri::Wry>>(); let menu = app.state::<tray::MenuItems>();
show_hide.set_text("Show")?; menu.after_hide()?;
Ok(()) Ok(())
} }

View File

@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use auto_launch::AutoLaunchBuilder; use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use is_terminal::IsTerminal; use is_terminal::IsTerminal;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
@ -89,29 +89,49 @@ impl AppConfig {
pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> { pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
kv::save(pool, "config", self).await kv::save(pool, "config", self).await
} }
}
/// Configure system with auto-launch settings
pub fn set_auto_launch(&self) -> Result<(), SetupError> {
let mgr = self.auto_launch_manager()?;
pub fn set_auto_launch(is_configured: bool) -> Result<(), SetupError> { // if enabled, disabled regardless of desired end state because either:
let path_buf = std::env::current_exe() // a) we are just going to leave it disabled, or
.map_err(|e| auto_launch::Error::Io(e))?; // b) we need to disable-and-reenable in case args are different
let path = path_buf if mgr.is_enabled()? {
.to_string_lossy(); mgr.disable()?;
}
if self.start_on_login {
mgr.enable()?;
}
let auto = AutoLaunchBuilder::new() Ok(())
.set_app_name("Creddy")
.set_app_path(&path)
.build()?;
let is_enabled = auto.is_enabled()?;
if is_configured && !is_enabled {
auto.enable()?;
}
else if !is_configured && is_enabled {
auto.disable()?;
} }
Ok(()) /// Match own auto-launch settings to system
pub async fn match_auto_launch(&mut self, pool: &SqlitePool) -> Result<(), SetupError> {
let mgr = self.auto_launch_manager()?;
let is_enabled = mgr.is_enabled()?;
if is_enabled != self.start_on_login {
self.start_on_login = is_enabled;
self.save(pool).await?;
}
Ok(())
}
fn auto_launch_manager(&self) -> Result<AutoLaunch, SetupError> {
let path_buf = std::env::current_exe()
.map_err(|e| auto_launch::Error::Io(e))?;
let name = if cfg!(debug_assertions) { "Creddy (dev)" } else { "Creddy" };
let mut builder = AutoLaunchBuilder::new();
builder.set_app_name(name);
builder.set_app_path(&path_buf.to_string_lossy());
if self.start_minimized {
builder.set_args(&["run", "--minimized"]);
}
Ok(builder.build()?)
}
} }

View File

@ -201,6 +201,10 @@ pub enum HandlerError {
Signature(#[from] signature::Error), Signature(#[from] signature::Error),
#[error(transparent)] #[error(transparent)]
Encoding(#[from] ssh_encoding::Error), Encoding(#[from] ssh_encoding::Error),
#[cfg(windows)]
#[error(transparent)]
Windows(#[from] windows::core::Error),
} }

View File

@ -204,6 +204,12 @@ pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<Stri
} }
#[tauri::command]
pub fn get_devmode() -> bool {
cfg!(debug_assertions)
}
#[tauri::command] #[tauri::command]
pub fn exit(app_handle: AppHandle) { pub fn exit(app_handle: AppHandle) {
app_handle.exit(0) app_handle.exit(0)

View File

@ -8,14 +8,23 @@ use creddy::{
app, app,
errors::ShowError, errors::ShowError,
}; };
use creddy_cli::{Action, Cli}; use creddy_cli::{
Action,
Cli,
RunArgs,
};
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let res = match cli.action { let res = match cli.action {
None | Some(Action::Run) => { None => {
app::run().error_popup("Creddy encountered an error"); let run_args = RunArgs { minimized: false };
app::run(run_args, cli.global_args).error_popup("Creddy encountered an error");
Ok(())
}
Some(Action::Run(run_args)) => {
app::run(run_args, cli.global_args).error_popup("Creddy encountered an error");
Ok(()) Ok(())
}, },
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args), Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),

View File

@ -44,10 +44,7 @@ fn launch_terminal() {
pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> Result<(), ShortcutError> { pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> Result<(), ShortcutError> {
let app = APP.get().unwrap(); let app = APP.get().unwrap();
let shortcuts = app.global_shortcut(); let shortcuts = app.global_shortcut();
shortcuts.unregister_all([ shortcuts.unregister_all()?;
hotkeys.show_window.keys.as_str(),
hotkeys.launch_terminal.keys.as_str(),
])?;
if hotkeys.show_window.enabled { if hotkeys.show_window.enabled {
shortcuts.on_shortcut( shortcuts.on_shortcut(

View File

@ -3,7 +3,9 @@ use std::future::Future;
use tauri::{ use tauri::{
AppHandle, AppHandle,
async_runtime as rt, async_runtime as rt,
Emitter,
Manager, Manager,
Runtime,
}; };
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::oneshot; use tokio::sync::oneshot;
@ -80,9 +82,11 @@ impl<'s> CloseWaiter<'s> {
} }
fn serve<H, F>(sock_name: &str, app_handle: AppHandle, handler: H) -> std::io::Result<()> // note: AppHandle is generic over `Runtime` for testing
where H: Copy + Send + Fn(Stream, AppHandle, u32) -> F + 'static, fn serve<H, F, R>(sock_name: &str, app_handle: AppHandle<R>, handler: H) -> std::io::Result<()>
where H: Copy + Send + Fn(Stream, AppHandle<R>, u32) -> F + 'static,
F: Send + Future<Output = Result<(), HandlerError>>, F: Send + Future<Output = Result<(), HandlerError>>,
R: Runtime
{ {
let (mut listener, addr) = platform::bind(sock_name)?; let (mut listener, addr) = platform::bind(sock_name)?;
rt::spawn(async move { rt::spawn(async move {
@ -185,6 +189,7 @@ mod platform {
#[cfg(windows)] #[cfg(windows)]
mod platform { mod platform {
use std::os::windows::io::AsRawHandle; use std::os::windows::io::AsRawHandle;
use std::path::PathBuf;
use tokio::net::windows::named_pipe::{ use tokio::net::windows::named_pipe::{
NamedPipeServer, NamedPipeServer,
ServerOptions, ServerOptions,
@ -198,7 +203,7 @@ mod platform {
pub type Stream = NamedPipeServer; pub type Stream = NamedPipeServer;
pub fn bind(sock_name: &str) -> std::io::Result<(String, NamedPipeServer)> { pub fn bind(sock_name: &str) -> std::io::Result<(NamedPipeServer, PathBuf)> {
let addr = creddy_cli::server_addr(sock_name); let addr = creddy_cli::server_addr(sock_name);
let listener = ServerOptions::new() let listener = ServerOptions::new()
.first_pipe_instance(true) .first_pipe_instance(true)
@ -206,7 +211,7 @@ mod platform {
Ok((listener, addr)) Ok((listener, addr))
} }
pub async fn accept(listener: &mut NamedPipeServer, addr: &String) -> Result<(NamedPipeServer, u32), HandlerError> { pub async fn accept(listener: &mut NamedPipeServer, addr: &PathBuf) -> Result<(NamedPipeServer, u32), HandlerError> {
// connect() just waits for a client to connect, it doesn't return anything // connect() just waits for a client to connect, it doesn't return anything
listener.connect().await?; listener.connect().await?;
@ -223,3 +228,31 @@ mod platform {
Ok((stream, pid)) Ok((stream, pid))
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn test_server_connect() {
let app = tauri::test::mock_app();
serve("creddy_server_test", app.app_handle().clone(), |mut stream, _handle, _pid| {
async move {
let buf = serde_json::to_vec(&CliResponse::Empty).unwrap();
stream.write_all(&buf).await.unwrap();
Ok(())
}
}).unwrap();
let addr = creddy_cli::server_addr("creddy_server_test");
let mut stream = creddy_cli::connect(Some(addr)).await.unwrap();
let mut buf = Vec::new();
stream.read_to_end(&mut buf).await.unwrap();
let resp: CliResponse = serde_json::from_slice(&buf).unwrap();
assert!(matches!(resp, CliResponse::Empty))
}
}

View File

@ -11,6 +11,7 @@ use ssh_agent_lib::proto::message::Identity;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use sqlx::types::Uuid; use sqlx::types::Uuid;
use tauri::{ use tauri::{
Emitter,
Manager, Manager,
async_runtime as rt, async_runtime as rt,
}; };
@ -22,7 +23,7 @@ use crate::credentials::{
DockerCredential, DockerCredential,
SshKey, SshKey,
}; };
use crate::{config, config::AppConfig}; use crate::config::AppConfig;
use crate::credentials::{ use crate::credentials::{
AwsBaseCredential, AwsBaseCredential,
Credential, Credential,
@ -32,6 +33,7 @@ use crate::credentials::{
use crate::ipc::{self, RequestResponse}; use crate::ipc::{self, RequestResponse};
use crate::errors::*; use crate::errors::*;
use crate::shortcuts; use crate::shortcuts;
use crate::tray;
#[derive(Debug)] #[derive(Debug)]
@ -203,8 +205,9 @@ impl AppState {
let mut live_config = self.config.write().await; let mut live_config = self.config.write().await;
// update autostart if necessary // update autostart if necessary
if new_config.start_on_login != live_config.start_on_login { if new_config.start_on_login != live_config.start_on_login
config::set_auto_launch(new_config.start_on_login)?; || new_config.start_minimized != live_config.start_minimized {
new_config.set_auto_launch()?;
} }
// re-register hotkeys if necessary // re-register hotkeys if necessary
@ -252,7 +255,11 @@ impl AppState {
pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> { pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
let mut session = self.app_session.write().await; let mut session = self.app_session.write().await;
session.unlock(passphrase) session.unlock(passphrase)?;
let app_handle = app::APP.get().unwrap();
let menu = app_handle.state::<tray::MenuItems>();
let _ = menu.after_unlock(); // we don't care if this fails, it's non-essential
Ok(())
} }
pub async fn lock(&self) -> Result<(), LockError> { pub async fn lock(&self) -> Result<(), LockError> {
@ -266,6 +273,9 @@ impl AppState {
let app_handle = app::APP.get().unwrap(); let app_handle = app::APP.get().unwrap();
app_handle.emit("locked", None::<usize>)?; app_handle.emit("locked", None::<usize>)?;
let menu = app_handle.state::<tray::MenuItems>();
let _ = menu.after_lock();
Ok(()) Ok(())
} }
} }

View File

@ -1,7 +1,11 @@
use std::process::Command; use std::process::Command;
use std::time::Duration; use std::time::Duration;
use tauri::{AppHandle, Manager}; use tauri::{
AppHandle,
Listener,
Manager,
};
use tokio::time::sleep; use tokio::time::sleep;
use crate::app::APP; use crate::app::APP;

View File

@ -7,27 +7,78 @@ use tauri::{
use tauri::menu::{ use tauri::menu::{
MenuBuilder, MenuBuilder,
MenuEvent, MenuEvent,
MenuItem,
MenuItemBuilder, MenuItemBuilder,
PredefinedMenuItem,
}; };
use tauri::tray::TrayIconBuilder;
use crate::app; use crate::app;
use crate::state::AppState; use crate::state::AppState;
pub struct MenuItems {
pub status: MenuItem<tauri::Wry>,
pub show_hide: MenuItem<tauri::Wry>,
}
impl MenuItems {
pub fn after_show(&self) -> tauri::Result<()> {
self.show_hide.set_text("Hide")
}
pub fn after_hide(&self) -> tauri::Result<()> {
self.show_hide.set_text("Show")
}
pub fn after_lock(&self) -> tauri::Result<()> {
if cfg!(debug_assertions) {
self.status.set_text("Creddy (dev): Locked")
}
else {
self.status.set_text("Creddy: Locked")
}
}
pub fn after_unlock(&self) -> tauri::Result<()> {
if cfg!(debug_assertions) {
self.status.set_text("Creddy (dev): Unlocked")
}
else {
self.status.set_text("Creddy: Unlocked")
}
}
}
pub fn setup(app: &App) -> tauri::Result<()> { pub fn setup(app: &App) -> tauri::Result<()> {
let status_text =
if cfg!(debug_assertions) {
"Creddy (dev): Locked"
}
else {
"Creddy: Locked"
};
let status = MenuItemBuilder::with_id("status", status_text)
.enabled(false)
.build(app)?;
let sep = PredefinedMenuItem::separator(app)?;
let show_hide = MenuItemBuilder::with_id("show_hide", "Show").build(app)?; let show_hide = MenuItemBuilder::with_id("show_hide", "Show").build(app)?;
let exit = MenuItemBuilder::with_id("exit", "Exit").build(app)?; let exit = MenuItemBuilder::with_id("exit", "Exit").build(app)?;
let menu = MenuBuilder::new(app) let menu = MenuBuilder::new(app)
.items(&[&show_hide, &exit]) .items(&[&status, &sep, &show_hide, &exit])
.build()?; .build()?;
let tray = app.tray_by_id("main").unwrap(); TrayIconBuilder::new()
tray.set_menu(Some(menu))?; .icon(app.default_window_icon().unwrap().clone())
tray.on_menu_event(handle_event); .menu(&menu)
.on_menu_event(handle_event)
.build(app)?;
// stash this so we can find it later to change the text // stash these so we can find them later to change the text
app.manage(show_hide); app.manage(MenuItems { status, show_hide });
Ok(()) Ok(())
} }

View File

@ -50,7 +50,7 @@
} }
}, },
"productName": "creddy", "productName": "creddy",
"version": "0.5.4", "version": "0.6.5",
"identifier": "creddy", "identifier": "creddy",
"plugins": {}, "plugins": {},
"app": { "app": {
@ -65,11 +65,6 @@
"visible": false "visible": false
} }
], ],
"trayIcon": {
"id": "main",
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"security": { "security": {
"csp": { "csp": {
"style-src": [ "style-src": [

View File

@ -14,6 +14,7 @@ import Unlock from './views/Unlock.svelte';
// set up app state // set up app state
invoke('get_config').then(config => $appState.config = config); invoke('get_config').then(config => $appState.config = config);
invoke('get_session_status').then(status => $appState.sessionStatus = status); invoke('get_session_status').then(status => $appState.sessionStatus = status);
invoke('get_devmode').then(dm => $appState.devmode = dm)
getVersion().then(version => $appState.appVersion = version); getVersion().then(version => $appState.appVersion = version);
invoke('get_setup_errors') invoke('get_setup_errors')
.then(errs => { .then(errs => {
@ -70,3 +71,9 @@ acceptRequest();
<!-- normal operation --> <!-- normal operation -->
<svelte:component this="{$currentView}" /> <svelte:component this="{$currentView}" />
{/if} {/if}
{#if $appState.devmode }
<div class="fixed left-0 bottom-0 right-0 py-1 bg-warning text-xs text-center text-warning-content">
This is a development build of Creddy.
</div>
{/if}

View File

@ -1,7 +1,7 @@
<script> <script>
// import { listen } from '@tauri-apps/api/event'; // import { listen } from '@tauri-apps/api/event';
import { open } from '@tauri-apps/plugin-dialog'; import { open } from '@tauri-apps/plugin-dialog';
import { sep } from '@tauri-apps/api/path'; import { basename } from '@tauri-apps/api/path';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Icon from './Icon.svelte'; import Icon from './Icon.svelte';
@ -14,17 +14,16 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function chooseFile() { async function chooseFile() {
let file = await open(params); let path = await open(params);
if (file) { if (path) {
value = file; displayValue = await basename(path);
displayValue = file.name; value = {name: displayValue, path};
dispatch('update', value); dispatch('update', value);
} }
} }
function handleInput(evt) { async function handleInput(evt) {
const segments = evt.target.value.split(sep()); const name = await basename(evt.target.value);
const name = segments[segments.length - 1];
value = {name, path: evt.target.value}; value = {name, path: evt.target.value};
} }

View File

@ -91,7 +91,7 @@
{#if launchTerminalError} {#if launchTerminalError}
<div class="toast"> <div class="toast">
<div class="alert alert-error shadow-lg"> <div class="alert alert-error text-wrap shadow-lg">
<span>{launchTerminalError.msg || launchTerminalError}</span> <span>{launchTerminalError.msg || launchTerminalError}</span>
<div> <div>
<button class="btn btn-alert-error" on:click={() => launchTerminalError = null}> <button class="btn btn-alert-error" on:click={() => launchTerminalError = null}>

View File

@ -144,7 +144,7 @@
{:else if records !== null} {:else if records !== null}
<div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6"> <div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6">
<div>You have no saved Docker credentials.</div> <div>You have no saved Docker credentials.</div>
<button class="btn btn-primary btn-wide mx-auto" on:click={newSsh}> <button class="btn btn-primary btn-wide mx-auto" on:click={newDocker}>
<Icon name="plus-circle-mini" class="size-5" /> <Icon name="plus-circle-mini" class="size-5" />
Add Add
</button> </button>

View File

@ -20,7 +20,6 @@
let error = null; let error = null;
async function save() { async function save() {
try { try {
throw('wtf');
await invoke('save_config', {config}); await invoke('save_config', {config});
$appState.config = await invoke('get_config'); $appState.config = await invoke('get_config');
} }
@ -48,11 +47,13 @@
</svelte:fragment> </svelte:fragment>
</ToggleSetting> </ToggleSetting>
<ToggleSetting title="Start minimized" bind:value={config.start_minimized}> {#if config.start_on_login}
<svelte:fragment slot="description"> <ToggleSetting title="Start minimized" bind:value={config.start_minimized}>
Minimize to the system tray at startup. <svelte:fragment slot="description">
</svelte:fragment> Minimize to the system tray when starting on login.
</ToggleSetting> </svelte:fragment>
</ToggleSetting>
{/if}
<NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds"> <NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds">
<svelte:fragment slot="description"> <svelte:fragment slot="description">
@ -114,7 +115,7 @@
{#if error} {#if error}
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast"> <div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
<div class="alert alert-error no-animation"> <div class="alert alert-error no-animation text-wrap">
<div> <div>
<span>{error}</span> <span>{error}</span>
</div> </div>
@ -126,7 +127,7 @@
</div> </div>
{:else if configModified} {:else if configModified}
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast"> <div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
<div class="alert shadow-lg no-animation"> <div class="alert shadow-lg no-animation text-wrap">
<span>You have unsaved changes.</span> <span>You have unsaved changes.</span>
<div> <div>