Compare commits

..

24 Commits

Author SHA1 Message Date
Gregory Schier
aadfbfdfca Fix lint errors 2025-06-10 08:16:02 -07:00
Gregory Schier
383fd05c6c Split appearance settings into theme/interface 2025-06-09 21:19:44 -07:00
Gregory Schier
be0a8fc27a Add proxy bypass setting and rewrite proxy logic 2025-06-09 14:29:12 -07:00
Gregory Schier
648a1ac53c Update DEVELOPMENT.md 2025-06-08 22:49:43 -07:00
Gregory Schier
9fab37fb17 Custom font selection (#226) 2025-06-08 22:48:27 -07:00
Gregory Schier
e0aaa33ccb Update README 2025-06-08 08:17:11 -07:00
Gregory Schier
20f7d20031 Enable socks reqwest feature 2025-06-08 08:10:55 -07:00
Gregory Schier
4d90bc78b1 Link docs in readme 2025-06-08 08:05:59 -07:00
Gregory Schier
97763a1301 Add README to types package 2025-06-08 08:03:11 -07:00
Gregory Schier
d8b5a201b6 I'm stupid 2025-06-07 20:17:28 -07:00
Gregory Schier
88e87a1999 Fix stupid typo 2025-06-07 20:15:32 -07:00
Gregory Schier
2c4c1abd20 Pin tauri cli 2025-06-07 20:04:04 -07:00
Gregory Schier
67026fc5b3 Tweak 2025-06-07 19:37:28 -07:00
Gregory Schier
423a1a0a52 Fix environment color editing 2025-06-07 19:32:23 -07:00
Gregory Schier
1abe01aa5a Embed migrations into Rust binary 2025-06-07 19:25:36 -07:00
Gregory Schier
d0fde99b1c Environment colors (#225) 2025-06-07 18:21:54 -07:00
Gregory Schier
27901231dc Clarify proxy HTTP/HTTPS setting 2025-06-06 20:34:23 -07:00
Gregory Schier
1d9d80319b Upgrade dependencies 2025-06-06 19:32:25 -07:00
Gregory Schier
f62e90297d Fix recent workspaces when open in new window 2025-06-06 14:10:30 -07:00
Gregory Schier
fcda6f8d32 Fix lint errors 2025-06-04 11:33:10 -07:00
Gregory Schier
021f2171d6 Show error dialog on migration failure 2025-06-04 11:20:28 -07:00
Gregory Schier
2562cf7c55 Setting to colorize HTTP methods
https://feedback.yaak.app/p/support-colors-for-http-method-in-sidebar
2025-06-04 10:59:40 -07:00
Gregory Schier
58873ea606 Fix text streaming breaking scroll 2025-06-04 10:38:55 -07:00
Gregory Schier
9d9e83d59f Remove sqlx for migrations (#224) 2025-06-03 14:42:32 -07:00
124 changed files with 2432 additions and 2200 deletions

View File

@@ -47,15 +47,12 @@ npm start
New migrations can be created from the `src-tauri/` directory:
```shell
cd src-tauri
sqlx migrate add migration-name
npm run migration
```
Run the app to apply the migrations.
Rerun the app to apply the migrations.
If nothing happens, try `cargo clean` and run the app again.
_Note: Development builds use a separate database location from production builds._
_Note: For safety, development builds use a separate database location from production builds._
## Lezer Grammer Generation

72
package-lock.json generated
View File

@@ -36,6 +36,7 @@
"plugins/template-function-xml",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -50,7 +51,7 @@
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tauri-apps/cli": "2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
@@ -63,7 +64,7 @@
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"workspaces-run": "^1.0.2"
}
},
@@ -2801,9 +2802,9 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
"integrity": "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz",
"integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -3037,36 +3038,36 @@
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.1.tgz",
"integrity": "sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.2.tgz",
"integrity": "sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-fs": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.2.1.tgz",
"integrity": "sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.3.0.tgz",
"integrity": "sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-log": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.3.1.tgz",
"integrity": "sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.4.0.tgz",
"integrity": "sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz",
"integrity": "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==",
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.7.tgz",
"integrity": "sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
@@ -3606,6 +3607,10 @@
"resolved": "src-tauri/yaak-crypto",
"link": true
},
"node_modules/@yaakapp-internal/fonts": {
"resolved": "src-tauri/yaak-fonts",
"link": true
},
"node_modules/@yaakapp-internal/git": {
"resolved": "src-tauri/yaak-git",
"link": true
@@ -12946,6 +12951,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-colorful": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
@@ -16097,9 +16112,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -17284,7 +17299,7 @@
},
"packages/plugin-runtime-types": {
"name": "@yaakapp/api",
"version": "0.6.0",
"version": "0.6.4",
"dependencies": {
"@types/node": "^22.5.4"
},
@@ -17533,6 +17548,10 @@
"name": "@yaakapp-internal/crypto",
"version": "1.0.0"
},
"src-tauri/yaak-fonts": {
"name": "@yaakapp-internal/fonts",
"version": "1.0.0"
},
"src-tauri/yaak-git": {
"name": "@yaakapp-internal/git",
"version": "1.0.0"
@@ -17598,12 +17617,12 @@
"@tanstack/react-query": "^5.76.1",
"@tanstack/react-router": "^1.120.3",
"@tanstack/react-virtual": "^3.13.8",
"@tauri-apps/api": "^2.4.1",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-log": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-log": "^2.4.0",
"@tauri-apps/plugin-opener": "^2.2.7",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-shell": "^2.2.1",
"buffer": "^6.0.3",
@@ -17626,6 +17645,7 @@
"papaparse": "^5.4.1",
"parse-color": "^1.0.0",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",

View File

@@ -35,6 +35,7 @@
"plugins/template-function-xml",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -49,6 +50,7 @@
"start": "npm run app-dev",
"app-build": "tauri build",
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
"migration": "node scripts/create-migration.cjs",
"build": "npm run --workspaces --if-present build",
"build-plugins": "npm run --workspaces --if-present build",
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
@@ -65,7 +67,7 @@
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tauri-apps/cli": "2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
@@ -78,7 +80,7 @@
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"workspaces-run": "^1.0.2"
}
}

View File

@@ -0,0 +1,28 @@
# Yaak Plugin API
Yaak is a desktop [API client](https://yaak.app/blog/yet-another-api-client) for
interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC APIs. It's
built using Tauri, Rust, and ReactJS.
Plugins can be created in TypeScript, which are executed alongside Yaak in a NodeJS
runtime. This package contains the TypeScript type definitions required to make building
Yaak plugins a breeze.
## Quick Start
The easiest way to get started is by generating a plugin with the Yaak CLI:
```shell
npx @yaakapp/cli generate
```
For more details on creating plugins, check out
the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-quick-start)
## Installation
If you prefer starting from scratch, manually install the types package:
```shell
npm install @yaakapp/api
```

View File

@@ -1,6 +1,20 @@
{
"name": "@yaakapp/api",
"version": "0.6.0",
"version": "0.6.4",
"keywords": [
"api-client",
"insomnia-alternative",
"bruno-alternative",
"postman-alternative"
],
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak"
},
"bugs": {
"url": "https://feedback.yaak.app"
},
"homepage": "https://yaak.app",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -1,12 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -0,0 +1,46 @@
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const slugify = require('slugify');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function generateTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
async function createMigration() {
try {
const migrationName = await new Promise((resolve) => {
rl.question('Enter migration name: ', resolve);
});
const timestamp = generateTimestamp();
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
const migrationsDir = path.join(__dirname, '../src-tauri/yaak-models/migrations');
const filePath = path.join(migrationsDir, fileName);
if (!fs.existsSync(migrationsDir)) {
fs.mkdirSync(migrationsDir, { recursive: true });
}
fs.writeFileSync(filePath, '-- Add migration SQL here\n');
console.log(`Created migration file: ${fileName}`);
} catch (error) {
console.error('Error creating migration:', error);
} finally {
rl.close();
}
}
createMigration().catch(console.error);

3262
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ members = [
"yaak-crypto",
"yaak-git",
"yaak-grpc",
"yaak-fonts",
"yaak-http",
"yaak-license",
"yaak-mac-window",
@@ -33,7 +34,7 @@ strip = true # Automatically strip symbols from the binary.
cargo-clippy = []
[build-dependencies]
tauri-build = { version = "2.1.1", features = [] }
tauri-build = { version = "2.2.0", features = [] }
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
@@ -48,13 +49,13 @@ log = "0.4.27"
md5 = "0.7.0"
mime_guess = "2.0.5"
rand = "0.9.0"
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks"] }
reqwest_cookie_store = "0.8.0"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-dialog = { workspace = true }
tauri-plugin-fs = "2.2.0"
tauri-plugin-log = { version = "2.3.1", features = ["colored"] }
tauri-plugin-opener = "2.2.6"
@@ -75,6 +76,7 @@ yaak-http = { workspace = true }
yaak-license = { path = "yaak-license" }
yaak-mac-window = { path = "yaak-mac-window" }
yaak-models = { workspace = true }
yaak-fonts = { workspace = true }
yaak-plugins = { workspace = true }
yaak-sse = { workspace = true }
yaak-sync = { workspace = true }
@@ -82,18 +84,20 @@ yaak-templates = { workspace = true }
yaak-ws = { path = "yaak-ws" }
[workspace.dependencies]
reqwest = "0.12.15"
reqwest = "0.12.19"
serde = "1.0.219"
serde_json = "1.0.140"
tauri = "2.4.1"
tauri-plugin = "2.1.1"
tauri = "2.5.1"
tauri-plugin = "2.2.0"
tauri-plugin-dialog = "2.2.2"
tauri-plugin-shell = "2.2.1"
tokio = "1.44.2"
tokio = "1.45.1"
thiserror = "2.0.12"
ts-rs = "10.1.0"
rustls = { version = "0.23.25", default-features = false }
rustls-platform-verifier = "0.5.1"
ts-rs = "11.0.1"
rustls = { version = "0.23.27", default-features = false }
rustls-platform-verifier = "0.6.0"
yaak-common = { path = "yaak-common" }
yaak-fonts = { path = "yaak-fonts" }
yaak-http = { path = "yaak-http" }
yaak-models = { path = "yaak-models" }
yaak-plugins = { path = "yaak-plugins" }

View File

@@ -53,6 +53,7 @@
"shell:allow-open",
"yaak-crypto:default",
"yaak-git:default",
"yaak-fonts:default",
"yaak-license:default",
"yaak-mac-window:default",
"yaak-models:default",

View File

@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderName, HeaderValue};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::{Method, Response};
use reqwest::{Method, NoProxy, Response};
use reqwest::{Proxy, Url, multipart};
use serde_json::Value;
use std::collections::BTreeMap;
@@ -120,25 +120,39 @@ pub async fn send_http_request<R: Runtime>(
https,
auth,
disabled,
bypass,
}) if !disabled => {
debug!("Using proxy http={http} https={https}");
let mut proxy = Proxy::custom(move |url| {
let http = if http.is_empty() { None } else { Some(http.to_owned()) };
let https = if https.is_empty() { None } else { Some(https.to_owned()) };
let proxy_url = match (url.scheme(), http, https) {
("http", Some(proxy_url), _) => Some(proxy_url),
("https", _, Some(proxy_url)) => Some(proxy_url),
_ => None,
debug!("Using proxy http={http} https={https} bypass={bypass}");
if !http.is_empty() {
match Proxy::http(http) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth.clone() {
debug!("Using http proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply http proxy {e:?}");
}
};
}
if !https.is_empty() {
match Proxy::https(https) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using https proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply https proxy {e:?}");
}
};
proxy_url
});
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
client_builder = client_builder.proxy(proxy);
}
_ => {} // Nothing to do for this one, as it is the default
}

View File

@@ -1265,6 +1265,7 @@ pub fn run() {
.plugin(yaak_models::init())
.plugin(yaak_plugins::init())
.plugin(yaak_crypto::init())
.plugin(yaak_fonts::init())
.plugin(yaak_git::init())
.plugin(yaak_ws::init())
.plugin(yaak_sync::init());

View File

@@ -57,7 +57,6 @@
],
"longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
"resources": [
"migrations",
"vendored/protoc/include",
"vendored/plugins",
"vendored/plugin-runtime"

View File

@@ -0,0 +1,16 @@
[package]
name = "yaak-fonts"
links = "yaak-fonts"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
font-loader = "0.11.0"
tauri = { workspace = true }
ts-rs = { workspace = true }
serde = "1.0"
thiserror = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Fonts = { editorFonts: Array<string>, uiFonts: Array<string>, };

View File

@@ -0,0 +1,5 @@
const COMMANDS: &[&str] = &["list"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}

View File

@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { Fonts } from './bindings/gen_fonts';
export async function listFonts() {
return invoke<Fonts>('plugin:yaak-fonts|list', {});
}
export function useFonts() {
return useQuery({
queryKey: ['list_fonts'],
queryFn: () => listFonts(),
});
}

View File

@@ -0,0 +1,6 @@
{
"name": "@yaakapp-internal/fonts",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -0,0 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-list"]

View File

@@ -0,0 +1,41 @@
use crate::Result;
use font_loader::system_fonts;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tauri::command;
use ts_rs::TS;
#[derive(Default, Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_fonts.ts")]
pub struct Fonts {
pub editor_fonts: Vec<String>,
pub ui_fonts: Vec<String>,
}
#[command]
pub(crate) async fn list() -> Result<Fonts> {
let mut ui_fonts = HashSet::new();
let mut editor_fonts = HashSet::new();
let mut property = system_fonts::FontPropertyBuilder::new().monospace().build();
for font in &system_fonts::query_specific(&mut property) {
editor_fonts.insert(font.to_string());
}
for font in &system_fonts::query_all() {
if !editor_fonts.contains(font) {
ui_fonts.insert(font.to_string());
}
}
let mut ui_fonts: Vec<String> = ui_fonts.into_iter().collect();
let mut editor_fonts: Vec<String> = editor_fonts.into_iter().collect();
ui_fonts.sort();
editor_fonts.sort();
Ok(Fonts {
ui_fonts,
editor_fonts,
})
}

View File

@@ -0,0 +1,15 @@
use serde::{ser::Serializer, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum Error {}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -0,0 +1,15 @@
use tauri::{
generate_handler,
plugin::{Builder, TauriPlugin},
Runtime,
};
mod commands;
mod error;
use crate::commands::list;
pub use error::{Error, Result};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-fonts").invoke_handler(generate_handler![list]).build()
}

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -8,9 +8,8 @@ publish = false
anyhow = "1.0.97"
async-recursion = "1.1.1"
dunce = "1.0.4"
hyper = "1.5.2"
hyper-rustls = { version = "0.27.5", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.10", default-features = false, features = ["client-legacy"] }
hyper-rustls = { version = "0.27.7", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.13", default-features = false, features = ["client-legacy"] }
log = "0.4.20"
md5 = "0.7.0"
prost = "0.13.4"
@@ -25,4 +24,4 @@ tokio-stream = "0.1.14"
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
tonic-reflection = "0.12.3"
uuid = { version = "1.7.0", features = ["v4"] }
yaak-http = { workspace = true }
yaak-http = { workspace = true }

View File

@@ -6,7 +6,7 @@ publish = false
[dependencies]
yaak-models = { workspace = true }
regex = "1.11.0"
regex = "1.11.1"
rustls = { workspace = true, default-features = false, features = ["ring"] }
rustls-platform-verifier = { workspace = true }
urlencoding = "2.1.3"

View File

@@ -1,9 +1,9 @@
use std::sync::Arc;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls::crypto::ring;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls_platform_verifier::BuilderVerifierExt;
use std::sync::Arc;
pub fn get_config(validate_certificates: bool) -> ClientConfig {
let arc_crypto_provider = Arc::new(ring::default_provider());
@@ -12,9 +12,7 @@ pub fn get_config(validate_certificates: bool) -> ClientConfig {
.unwrap();
if validate_certificates {
// Use platform-native verifier to validate certificates
config_builder
.with_platform_verifier()
.with_no_client_auth()
config_builder.with_platform_verifier().unwrap().with_no_client_auth()
} else {
config_builder
.dangerous()

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use hex_color::HexColor;
use objc::{msg_send, sel, sel_impl};
use tauri::{Emitter, Runtime, Window};
@@ -65,12 +66,7 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor
}
}
fn position_traffic_lights(
ns_window_handle: UnsafeWindowHandle,
x: f64,
y: f64,
label: String,
) {
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, label: String) {
if !label.starts_with(MAIN_WINDOW_PREFIX) {
return;
}

View File

@@ -7,6 +7,8 @@ publish = false
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
hex = "0.4.3"
include_dir = "0.7"
log = "0.4.22"
nanoid = "0.4.0"
r2d2 = "0.8.10"
@@ -16,8 +18,9 @@ sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sqlx = { version = "0.8.0", default-features = false, features = ["migrate", "sqlite", "runtime-tokio-rustls"] }
tauri = { workspace = true }
sha2 = "0.10.9"
tauri = { workspace = true}
tauri-plugin-dialog = { workspace = true }
thiserror = "2.0.11"
tokio = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }

View File

@@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -58,11 +58,11 @@ export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, bypass: string, } | { "type": "disabled" };
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };

View File

@@ -1,9 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models";
import type { Folder } from "./gen_models";
import type { GrpcRequest } from "./gen_models";
import type { HttpRequest } from "./gen_models";
import type { WebsocketRequest } from "./gen_models";
import type { Workspace } from "./gen_models";
import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models.js";
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };

View File

@@ -164,8 +164,6 @@ DROP TABLE websocket_requests;
ALTER TABLE websocket_requests_dg_tmp
RENAME TO websocket_requests;
PRAGMA foreign_keys = ON;
---------------------------
-- Remove environment FK --
---------------------------

View File

@@ -0,0 +1,2 @@
ALTER TABLE settings
ADD COLUMN colored_methods BOOLEAN DEFAULT FALSE;

View File

@@ -0,0 +1,2 @@
ALTER TABLE settings ADD COLUMN interface_font TEXT;
ALTER TABLE settings ADD COLUMN editor_font TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE environments ADD COLUMN color TEXT;

View File

@@ -20,6 +20,9 @@ pub enum Error {
#[error("Model error: {0}")]
GenericError(String),
#[error("DB Migration Failed: {0}")]
MigrationError(String),
#[error("No base environment for {0}")]
MissingBaseEnvironment(String),

View File

@@ -1,20 +1,16 @@
use crate::commands::*;
use crate::migrate::migrate_db;
use crate::query_manager::QueryManager;
use crate::util::ModelChangeEvent;
use log::info;
use log::error;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use sqlx::SqlitePool;
use sqlx::migrate::Migrator;
use sqlx::sqlite::SqliteConnectOptions;
use std::fs::create_dir_all;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use tauri::async_runtime::Mutex;
use tauri::path::BaseDirectory;
use tauri::plugin::TauriPlugin;
use tauri::{AppHandle, Emitter, Manager, Runtime, generate_handler};
use tauri::{Emitter, Manager, Runtime, generate_handler};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use tokio::sync::mpsc;
mod commands;
@@ -22,6 +18,7 @@ mod commands;
mod connection_or_tx;
pub mod db_context;
pub mod error;
mod migrate;
pub mod models;
pub mod queries;
pub mod query_manager;
@@ -55,13 +52,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
let db_file_path = app_path.join("db.sqlite");
{
let db_file_path = db_file_path.clone();
tauri::async_runtime::block_on(async move {
must_migrate_db(app_handle.app_handle(), &db_file_path).await;
});
};
let manager = SqliteConnectionManager::file(db_file_path);
let pool = Pool::builder()
.max_size(100) // Up from 10 (just in case)
@@ -69,6 +59,16 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.build(manager)
.unwrap();
if let Err(e) = migrate_db(&pool) {
error!("Failed to run database migration {e:?}");
app_handle
.dialog()
.message(e.to_string())
.kind(MessageDialogKind::Error)
.blocking_show();
return Err(Box::from(e.to_string()));
};
app_handle.manage(SqliteConnection::new(pool.clone()));
{
@@ -90,21 +90,3 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
})
.build()
}
async fn must_migrate_db<R: Runtime>(app_handle: &AppHandle<R>, sqlite_file_path: &PathBuf) {
info!("Connecting to database at {sqlite_file_path:?}");
let sqlite_file_path = sqlite_file_path.to_str().unwrap().to_string();
let opts = SqliteConnectOptions::from_str(&sqlite_file_path).unwrap().create_if_missing(true);
let pool = SqlitePool::connect_with(opts).await.expect("Failed to connect to database");
let p = app_handle
.path()
.resolve("migrations", BaseDirectory::Resource)
.expect("failed to resolve resource");
info!("Running database migrations from: {}", p.to_string_lossy());
let mut m = Migrator::new(p).await.expect("Failed to load migrations");
m.set_ignore_missing(true); // So we can roll back versions and not crash
m.run(&pool).await.expect("Failed to run migrations");
info!("Database migrations complete");
}

View File

@@ -0,0 +1,122 @@
use crate::error::Error::MigrationError;
use crate::error::Result;
use include_dir::{include_dir, Dir, DirEntry};
use log::{debug, info};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{params, OptionalExtension, TransactionBehavior};
use sha2::{Digest, Sha384};
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
pub(crate) fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
info!("Running database migrations");
// Ensure the table exists
// NOTE: Yaak used to use sqlx for migrations, so we need to mirror that table structure. We
// are writing checksum but not verifying because we want to be able to change migrations after
// a release in case something breaks.
pool.get()?.execute(
"CREATE TABLE IF NOT EXISTS _sqlx_migrations (
version BIGINT PRIMARY KEY,
description TEXT NOT NULL,
installed_on TIMESTAMP default CURRENT_TIMESTAMP NOT NULL,
success BOOLEAN NOT NULL,
checksum BLOB NOT NULL,
execution_time BIGINT NOT NULL
)",
[],
)?;
// Read and sort all .sql files
let mut entries = MIGRATIONS_DIR
.entries()
.into_iter()
.filter(|e| e.path().extension().map(|ext| ext == "sql").unwrap_or(false))
.collect::<Vec<_>>();
// Ensure they're in the correct order
entries.sort_by_key(|e| e.path());
// Run each migration in a transaction
let mut num_migrations = 0;
for entry in entries {
num_migrations += 1;
let mut conn = pool.get()?;
let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
match run_migration(entry, &mut tx) {
Ok(_) => tx.commit()?,
Err(e) => {
let msg = format!(
"{} failed with {}",
entry.path().file_name().unwrap().to_str().unwrap(),
e.to_string()
);
tx.rollback()?;
return Err(MigrationError(msg));
}
};
}
info!("Finished running {} migrations", num_migrations);
Ok(())
}
fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result<bool> {
let start = std::time::Instant::now();
let (version, description) = split_migration_filename(migration_path.path().to_str().unwrap())
.expect("Failed to parse migration filename");
// Skip if already applied
let row: Option<i64> = tx
.query_row("SELECT 1 FROM _sqlx_migrations WHERE version = ?", [version.clone()], |r| {
r.get(0)
})
.optional()?;
if row.is_some() {
debug!("Skipping migration {description}");
// Migration was already run
return Ok(false);
}
let sql =
migration_path.as_file().unwrap().contents_utf8().expect("Failed to read migration file");
info!("Applying migration {description}");
// Split on `;`? → optional depending on how your SQL is structured
tx.execute_batch(&sql)?;
let execution_time = start.elapsed().as_nanos() as i64;
let checksum = sha384_hex_prefixed(sql.as_bytes());
// NOTE: The success column is never used. It's just there for sqlx compatibility.
tx.execute(
"INSERT INTO _sqlx_migrations (version, description, execution_time, checksum, success) VALUES (?, ?, ?, ?, ?)",
params![version, description, execution_time, checksum, true],
)?;
Ok(true)
}
fn split_migration_filename(filename: &str) -> Option<(String, String)> {
// Remove the .sql extension
let trimmed = filename.strip_suffix(".sql")?;
// Split on the first underscore
let mut parts = trimmed.splitn(2, '_');
let version = parts.next()?.to_string();
let description = parts.next()?.to_string();
Some((version, description))
}
fn sha384_hex_prefixed(input: &[u8]) -> String {
let mut hasher = Sha384::new();
hasher.update(input);
let result = hasher.finalize();
// Format as 0x... with uppercase hex
format!("0x{}", hex::encode_upper(result))
}

View File

@@ -31,12 +31,15 @@ macro_rules! impl_model {
#[ts(export, export_to = "gen_models.ts")]
pub enum ProxySetting {
Enabled {
#[serde(default)]
// This was added after on so give it a default to be able to deserialize older values
disabled: bool,
http: String,
https: String,
auth: Option<ProxySettingAuth>,
// These were added later, so give them defaults
#[serde(default)]
bypass: String,
#[serde(default)]
disabled: bool,
},
Disabled,
}
@@ -103,9 +106,13 @@ pub struct Settings {
pub updated_at: NaiveDateTime,
pub appearance: String,
pub colored_methods: bool,
pub editor_font: Option<String>,
pub editor_font_size: i32,
pub editor_keymap: EditorKeymap,
pub editor_soft_wrap: bool,
pub hide_window_controls: bool,
pub interface_font: Option<String>,
pub interface_font_size: i32,
pub interface_scale: f32,
pub open_workspace_new_window: Option<bool>,
@@ -113,7 +120,6 @@ pub struct Settings {
pub theme_dark: String,
pub theme_light: String,
pub update_channel: String,
pub editor_keymap: EditorKeymap,
}
impl UpsertModelInfo for Settings {
@@ -153,6 +159,8 @@ impl UpsertModelInfo for Settings {
(EditorFontSize, self.editor_font_size.into()),
(EditorKeymap, self.editor_keymap.to_string().into()),
(EditorSoftWrap, self.editor_soft_wrap.into()),
(EditorFont, self.editor_font.into()),
(InterfaceFont, self.interface_font.into()),
(InterfaceFontSize, self.interface_font_size.into()),
(InterfaceScale, self.interface_scale.into()),
(HideWindowControls, self.hide_window_controls.into()),
@@ -160,6 +168,7 @@ impl UpsertModelInfo for Settings {
(ThemeDark, self.theme_dark.as_str().into()),
(ThemeLight, self.theme_light.as_str().into()),
(UpdateChannel, self.update_channel.into()),
(ColoredMethods, self.colored_methods.into()),
(Proxy, proxy.into()),
])
}
@@ -171,14 +180,17 @@ impl UpsertModelInfo for Settings {
SettingsIden::EditorFontSize,
SettingsIden::EditorKeymap,
SettingsIden::EditorSoftWrap,
SettingsIden::EditorFont,
SettingsIden::InterfaceFontSize,
SettingsIden::InterfaceScale,
SettingsIden::InterfaceFont,
SettingsIden::HideWindowControls,
SettingsIden::OpenWorkspaceNewWindow,
SettingsIden::Proxy,
SettingsIden::ThemeDark,
SettingsIden::ThemeLight,
SettingsIden::UpdateChannel,
SettingsIden::ColoredMethods,
]
}
@@ -195,16 +207,19 @@ impl UpsertModelInfo for Settings {
updated_at: row.get("updated_at")?,
appearance: row.get("appearance")?,
editor_font_size: row.get("editor_font_size")?,
editor_font: row.get("editor_font")?,
editor_keymap: EditorKeymap::from_str(editor_keymap.as_str()).unwrap(),
editor_soft_wrap: row.get("editor_soft_wrap")?,
interface_font_size: row.get("interface_font_size")?,
interface_scale: row.get("interface_scale")?,
interface_font: row.get("interface_font")?,
open_workspace_new_window: row.get("open_workspace_new_window")?,
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
theme_dark: row.get("theme_dark")?,
theme_light: row.get("theme_light")?,
hide_window_controls: row.get("hide_window_controls")?,
update_channel: row.get("update_channel")?,
colored_methods: row.get("colored_methods")?,
})
}
}
@@ -516,6 +531,7 @@ pub struct Environment {
pub public: bool,
pub base: bool,
pub variables: Vec<EnvironmentVariable>,
pub color: Option<String>,
}
impl UpsertModelInfo for Environment {
@@ -549,6 +565,7 @@ impl UpsertModelInfo for Environment {
(UpdatedAt, upsert_date(source, self.updated_at)),
(WorkspaceId, self.workspace_id.into()),
(Base, self.base.into()),
(Color, self.color.into()),
(Name, self.name.trim().into()),
(Public, self.public.into()),
(Variables, serde_json::to_string(&self.variables)?.into()),
@@ -559,6 +576,7 @@ impl UpsertModelInfo for Environment {
vec![
EnvironmentIden::UpdatedAt,
EnvironmentIden::Base,
EnvironmentIden::Color,
EnvironmentIden::Name,
EnvironmentIden::Public,
EnvironmentIden::Variables,
@@ -577,6 +595,7 @@ impl UpsertModelInfo for Environment {
created_at: row.get("created_at")?,
updated_at: row.get("updated_at")?,
base: row.get("base")?,
color: row.get("color")?,
name: row.get("name")?,
public: row.get("public")?,
variables: serde_json::from_str(variables.as_str()).unwrap_or_default(),

View File

@@ -19,16 +19,19 @@ impl<'a> DbContext<'a> {
appearance: "system".to_string(),
editor_font_size: 13,
editor_font: None,
editor_keymap: EditorKeymap::Default,
editor_soft_wrap: true,
interface_font_size: 15,
interface_scale: 1.0,
interface_font: None,
hide_window_controls: false,
open_workspace_new_window: None,
proxy: None,
theme_dark: "yaak-dark".to_string(),
theme_light: "yaak-light".to_string(),
update_channel: "stable".to_string(),
colored_methods: false,
};
self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings")
}

View File

@@ -1,12 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -1,6 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SyncModel } from "./gen_models.js";
import type { SyncState } from "./gen_models.js";
import type { SyncModel, SyncState } from "./gen_models.js";
export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, };

View File

@@ -236,7 +236,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
return workspaces;
}
const r = [...workspaces].sort((a, b) => {
return [...workspaces].sort((a, b) => {
const aRecentIndex = recentWorkspaces?.indexOf(a.id);
const bRecentIndex = recentWorkspaces?.indexOf(b.id);
@@ -250,7 +250,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
return a.createdAt.localeCompare(b.createdAt);
}
});
return r;
}, [recentWorkspaces, workspaces]);
const groups = useMemo<CommandPaletteGroup[]>(() => {
@@ -272,7 +271,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
searchText: resolvedModelNameWithFolders(r),
label: (
<HStack space={2}>
<HttpMethodTag className="text-text-subtlest" request={r} />
<HttpMethodTag short className="text-xs" request={r} />
<div className="truncate">{resolvedModelNameWithFolders(r)}</div>
</HStack>
),

View File

@@ -4,11 +4,12 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { toggleDialog } from '../lib/dialog';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { ButtonProps } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
type Props = {
@@ -38,6 +39,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
(e) => ({
key: e.id,
label: e.name,
rightSlot: <EnvironmentColorIndicator environment={e} />,
leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => {
if (e.id !== activeEnvironment?.id) {
@@ -80,6 +82,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined}
{...buttonProps}
>
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
{activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')}
</Button>
</Dropdown>

View File

@@ -0,0 +1,29 @@
import type { Environment } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { showColorPicker } from '../lib/showColorPicker';
export function EnvironmentColorIndicator({
environment,
clickToEdit,
}: {
environment: Environment | null;
clickToEdit?: boolean;
}) {
if (environment?.color == null) return null;
const style = { backgroundColor: environment.color };
const className =
'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent';
if (clickToEdit) {
return (
<button
onClick={() => showColorPicker(environment)}
style={style}
className={classNames(className, 'hover:border-text')}
/>
);
} else {
return <span style={style} className={className} />;
}
}

View File

@@ -0,0 +1,32 @@
import { useState } from 'react';
import { Button } from './core/Button';
import { ColorPicker } from './core/ColorPicker';
export function EnvironmentColorPicker({
color: defaultColor,
onChange,
}: {
color: string | null;
onChange: (color: string | null) => void;
}) {
const [color, setColor] = useState<string | null>(defaultColor);
return (
<form
className="flex flex-col items-stretch gap-3 pb-2 w-full"
onSubmit={(e) => {
e.preventDefault();
onChange(color);
}}
>
<ColorPicker color={color} onChange={setColor} />
<div className="grid grid-cols-[1fr_1fr] gap-1.5">
<Button variant="border" color="secondary" onClick={() => onChange(null)}>
Clear
</Button>
<Button type="submit" color="primary">
Save
</Button>
</div>
</form>
);
}

View File

@@ -17,6 +17,7 @@ import {
setupOrConfigureEncryption,
withEncryptionEnabled,
} from '../lib/setupOrConfigureEncryption';
import { showColorPicker } from '../lib/showColorPicker';
import { BadgeButton } from './core/BadgeButton';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
@@ -35,6 +36,7 @@ import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout';
import { VStack } from './core/Stacks';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
interface Props {
initialEnvironment: Environment | null;
@@ -123,7 +125,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
))}
{subEnvironments.length > 0 && (
<div className="px-2">
<Separator className="my-3"></Separator>
<Separator className="my-3" />
</div>
)}
{subEnvironments.map((e) => (
@@ -237,6 +239,7 @@ const EnvironmentEditor = function ({
return (
<VStack space={4} className={classNames(className, 'pl-4')}>
<Heading className="w-full flex items-center gap-0.5">
<EnvironmentColorIndicator clickToEdit environment={selectedEnvironment ?? null} />
<div className="mr-2">{selectedEnvironment?.name}</div>
{isEncryptionEnabled ? (
promptToEncrypt ? (
@@ -344,6 +347,7 @@ function SidebarButton({
onContextMenu={handleContextMenu}
rightSlot={rightSlot}
>
<EnvironmentColorIndicator environment={environment} />
{children}
</Button>
{outerRightSlot}
@@ -385,6 +389,12 @@ function SidebarButton({
},
]
: []) as DropdownItem[]),
{
label: environment.color ? 'Change Color' : 'Assign Color',
leftSlot: <Icon icon="palette" />,
hidden: environment.base,
onSelect: async () => showColorPicker(environment),
},
{
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,

View File

@@ -176,7 +176,6 @@ export function GrpcRequestPane({
<UrlBar
key={forceUpdateKey}
url={activeRequest.url ?? ''}
method={null}
submitIcon={null}
forceUpdateKey={forceUpdateKey}
placeholder="localhost:50051"

View File

@@ -41,8 +41,8 @@ import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import type { TabItem } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { FormMultipartEditor } from './FormMultipartEditor';
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
@@ -50,6 +50,7 @@ import { GraphQLEditor } from './GraphQLEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { RequestMethodDropdown } from './RequestMethodDropdown';
import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
@@ -138,10 +139,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ||
activeRequest.bodyType === BODY_TYPE_FORM_MULTIPART
) {
const n = Array.isArray(activeRequest.body?.form)
numParams = Array.isArray(activeRequest.body?.form)
? activeRequest.body.form.filter((p) => p.name).length
: 0;
numParams = n;
}
const tabs = useMemo<TabItem[]>(
@@ -314,11 +314,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
[activeRequest.id, sendRequest],
);
const handleMethodChange = useCallback(
(method: string) => patchModel(activeRequest, { method }),
[activeRequest],
);
const handleUrlChange = useCallback(
(url: string) => patchModel(activeRequest, { url }),
[activeRequest],
@@ -335,14 +330,17 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
stateKey={`url.${activeRequest.id}`}
key={forceUpdateKey + urlKey}
url={activeRequest.url}
method={activeRequest.method}
placeholder="https://example.com"
onPasteOverwrite={handlePaste}
autocomplete={autocomplete}
onSend={handleSend}
onCancel={cancelResponse}
onMethodChange={handleMethodChange}
onUrlChange={handleUrlChange}
leftSlot={
<div className="py-0.5">
<RequestMethodDropdown request={activeRequest} className="ml-0.5 !h-full" />
</div>
}
forceUpdateKey={updateKey}
isLoading={activeResponse != null && activeResponse.state !== 'closed'}
/>

View File

@@ -67,7 +67,7 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
...responses.map((r: HttpResponse) => ({
label: (
<HStack space={2}>
<HttpStatusTag className="text-sm" response={r} />
<HttpStatusTag short className="text-xs" response={r} />
<span className="text-text-subtle">&rarr;</span>{' '}
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span>
</HStack>

View File

@@ -58,7 +58,7 @@ export function RecentRequestsDropdown({ className }: Props) {
recentRequestItems.push({
label: resolvedModelName(request),
leftSlot: <HttpMethodTag request={request} />,
leftSlot: <HttpMethodTag short className="text-xs" request={request} />,
onSelect: async () => {
await router.navigate({
to: '/workspaces/$workspaceId',

View File

@@ -1,16 +1,18 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { showPrompt } from '../lib/prompt';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { HttpMethodTag } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import type { RadioDropdownItem } from './core/RadioDropdown';
import { RadioDropdown } from './core/RadioDropdown';
type Props = {
method: string;
request: HttpRequest;
className?: string;
onChange: (method: string) => void;
};
const radioItems: RadioDropdownItem<string>[] = [
@@ -28,10 +30,16 @@ const radioItems: RadioDropdownItem<string>[] = [
}));
export const RequestMethodDropdown = memo(function RequestMethodDropdown({
method,
onChange,
request,
className,
}: Props) {
const handleChange = useCallback(
async (method: string) => {
await patchModel(request, { method });
},
[request],
);
const itemsAfter = useMemo<DropdownItem[]>(
() => [
{
@@ -49,17 +57,22 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
placeholder: 'CUSTOM',
});
if (newMethod == null) return;
onChange(newMethod);
await handleChange(newMethod);
},
},
],
[onChange],
[handleChange],
);
return (
<RadioDropdown value={method} items={radioItems} itemsAfter={itemsAfter} onChange={onChange}>
<RadioDropdown
value={request.method}
items={radioItems}
itemsAfter={itemsAfter}
onChange={handleChange}
>
<Button size="xs" className={classNames(className, 'text-text-subtle hover:text')}>
{method.toUpperCase()}
<HttpMethodTag request={request} />
</Button>
</RadioDropdown>
);

View File

@@ -8,22 +8,24 @@ import { capitalize } from '../../lib/capitalize';
import { HStack } from '../core/Stacks';
import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { HeaderSize } from '../HeaderSize';
import { SettingsAppearance } from './SettingsAppearance';
import { SettingsInterface } from './SettingsInterface';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsLicense } from './SettingsLicense';
import { SettingsPlugins } from './SettingsPlugins';
import { SettingsProxy } from './SettingsProxy';
import { SettingsTheme } from './SettingsTheme';
interface Props {
hide?: () => void;
}
const TAB_GENERAL = 'general';
const TAB_APPEARANCE = 'appearance';
const TAB_INTERFACE = 'interface';
const TAB_THEME = 'theme';
const TAB_PROXY = 'proxy';
const TAB_PLUGINS = 'plugins';
const TAB_LICENSE = 'license';
const tabs = [TAB_GENERAL, TAB_APPEARANCE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE] as const;
const tabs = [TAB_GENERAL, TAB_THEME, TAB_INTERFACE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE] as const;
export type SettingsTab = (typeof tabs)[number];
export default function Settings({ hide }: Props) {
@@ -73,8 +75,11 @@ export default function Settings({ hide }: Props) {
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<SettingsGeneral />
</TabContent>
<TabContent value={TAB_APPEARANCE} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
<TabContent value={TAB_INTERFACE} className="pt-3 overflow-y-auto h-full px-4">
<SettingsInterface />
</TabContent>
<TabContent value={TAB_THEME} className="pt-3 overflow-y-auto h-full px-4">
<SettingsTheme />
</TabContent>
<TabContent value={TAB_PLUGINS} className="pt-3 overflow-y-auto h-full px-4">
<SettingsPlugins />

View File

@@ -0,0 +1,137 @@
import { type } from '@tauri-apps/plugin-os';
import { useFonts } from '@yaakapp-internal/fonts';
import type { EditorKeymap } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { Checkbox } from '../core/Checkbox';
import { Icon } from '../core/Icon';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
const NULL_FONT_VALUE = '__NULL_FONT__';
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
].map((n) => ({ label: `${n}`, value: `${n}` }));
const keymaps: { value: EditorKeymap; label: string }[] = [
{ value: 'default', label: 'Default' },
{ value: 'vim', label: 'Vim' },
{ value: 'vscode', label: 'VSCode' },
{ value: 'emacs', label: 'Emacs' },
];
export function SettingsInterface() {
const workspace = useAtomValue(activeWorkspaceAtom);
const settings = useAtomValue(settingsAtom);
const fonts = useFonts();
if (settings == null || workspace == null) {
return null;
}
return (
<VStack space={3} className="mb-4">
<HStack space={2} alignItems="end">
{fonts.data && (
<Select
size="sm"
name="uiFont"
label="Interface Font"
value={settings.interfaceFont ?? NULL_FONT_VALUE}
options={[
{ label: 'System Default', value: NULL_FONT_VALUE },
...(fonts.data.uiFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
// Some people like monospace fonts for the UI
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
]}
onChange={async (v) => {
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { interfaceFont });
}}
/>
)}
<Select
hideLabel
size="sm"
name="interfaceFontSize"
label="Interface Font Size"
defaultValue="15"
value={`${settings.interfaceFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { interfaceFontSize: parseInt(v) })}
/>
</HStack>
<HStack space={2} alignItems="end">
{fonts.data && (
<Select
size="sm"
name="editorFont"
label="Editor Font"
value={settings.editorFont ?? NULL_FONT_VALUE}
options={[
{ label: 'System Default', value: NULL_FONT_VALUE },
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
]}
onChange={async (v) => {
const editorFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { editorFont });
}}
/>
)}
<Select
hideLabel
size="sm"
name="editorFontSize"
label="Editor Font Size"
defaultValue="13"
value={`${settings.editorFontSize}`}
options={fontSizeOptions}
onChange={(v) =>
patchModel(settings, { editorFontSize: clamp(parseInt(v) || 14, 8, 30) })
}
/>
</HStack>
<Select
leftSlot={<Icon icon="keyboard" color="secondary" />}
size="sm"
name="editorKeymap"
label="Editor Keymap"
value={`${settings.editorKeymap}`}
options={keymaps}
onChange={(v) => patchModel(settings, { editorKeymap: v })}
/>
<Checkbox
checked={settings.editorSoftWrap}
title="Wrap Editor Lines"
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
<Checkbox
checked={settings.coloredMethods}
title="Colorize Request Methods"
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
/>
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide Window Controls"
help="Hide the close/maximize/minimize controls on Windows or Linux"
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
/>
)}
</VStack>
);
}

View File

@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { Checkbox } from '../core/Checkbox';
import { InlineCode } from '../core/InlineCode';
import { PlainInput } from '../core/PlainInput';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
@@ -29,6 +30,7 @@ export function SettingsProxy() {
http: '',
https: '',
auth: { user: '', password: '' },
bypass: '',
},
});
} else {
@@ -52,22 +54,28 @@ export function SettingsProxy() {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const disabled = !enabled;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
<HStack space={1.5}>
<PlainInput
size="sm"
label="HTTP"
label={
<>
Proxy for <InlineCode>http://</InlineCode> traffic
</>
}
placeholder="localhost:9090"
defaultValue={settings.proxy?.http}
onChange={async (http) => {
const { proxy } = settings;
const https = proxy?.type === 'enabled' ? proxy.https : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
await patchModel(settings, {
@@ -77,22 +85,28 @@ export function SettingsProxy() {
https,
auth,
disabled,
bypass,
},
});
}}
/>
<PlainInput
size="sm"
label="HTTPS"
label={
<>
Proxy for <InlineCode>https://</InlineCode> traffic
</>
}
placeholder="localhost:9090"
defaultValue={settings.proxy?.https}
onChange={async (https) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
@@ -106,9 +120,10 @@ export function SettingsProxy() {
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = enabled ? { user: '', password: '' } : null;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
@@ -126,10 +141,11 @@ export function SettingsProxy() {
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
@@ -144,15 +160,39 @@ export function SettingsProxy() {
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
</HStack>
)}
{settings.proxy.type === 'enabled' && (
<>
<Separator className="my-6" />
<PlainInput
label="Proxy Bypass"
help="Comma-separated list to bypass the proxy."
defaultValue={settings.proxy.bypass}
placeholder="127.0.0.1, *.example.com, localhost:3000"
onChange={async (bypass) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
</>
)}
</VStack>
)}
</VStack>

View File

@@ -1,35 +1,19 @@
import type { EditorKeymap } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
import { clamp } from '../../lib/clamp';
import { getThemes } from '../../lib/theme/themes';
import { isThemeDark } from '../../lib/theme/window';
import type { ButtonProps } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { Editor } from '../core/Editor/Editor';
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import type { SelectProps } from '../core/Select';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import type { SelectProps } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
import { type } from '@tauri-apps/plugin-os';
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
].map((n) => ({ label: `${n}`, value: `${n}` }));
const keymaps: { value: EditorKeymap; label: string }[] = [
{ value: 'default', label: 'Default' },
{ value: 'vim', label: 'Vim' },
{ value: 'vscode', label: 'VSCode' },
{ value: 'emacs', label: 'Emacs' },
];
const buttonColors: ButtonProps['color'][] = [
'primary',
@@ -62,7 +46,7 @@ const icons: IconProps['icon'][] = [
const { themes } = getThemes();
export function SettingsAppearance() {
export function SettingsTheme() {
const workspace = useAtomValue(activeWorkspaceAtom);
const settings = useAtomValue(settingsAtom);
const appearance = useResolvedAppearance();
@@ -87,53 +71,7 @@ export function SettingsAppearance() {
}));
return (
<VStack space={2} className="mb-4">
<Select
size="sm"
name="interfaceFontSize"
label="Font Size"
labelPosition="left"
defaultValue="15"
value={`${settings.interfaceFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { interfaceFontSize: parseInt(v) })}
/>
<Select
size="sm"
name="editorFontSize"
label="Editor Font Size"
labelPosition="left"
defaultValue="13"
value={`${settings.editorFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { editorFontSize: clamp(parseInt(v) || 14, 8, 30) })}
/>
<Select
size="sm"
name="editorKeymap"
label="Editor Keymap"
labelPosition="left"
value={`${settings.editorKeymap}`}
options={keymaps}
onChange={(v) => patchModel(settings, { editorKeymap: v })}
/>
<Checkbox
checked={settings.editorSoftWrap}
title="Wrap Editor Lines"
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide Window Controls"
help="Hide the close/maximize/minimize controls on Windows or Linux"
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
/>
)}
<Separator className="my-4" />
<VStack space={3} className="mb-4">
<Select
name="appearance"
label="Appearance"
@@ -151,7 +89,7 @@ export function SettingsAppearance() {
{(settings.appearance === 'system' || settings.appearance === 'light') && (
<Select
hideLabel
leftSlot={<Icon icon="sun" />}
leftSlot={<Icon icon="sun" color="secondary" />}
name="lightTheme"
label="Light Theme"
size="sm"
@@ -167,7 +105,7 @@ export function SettingsAppearance() {
name="darkTheme"
className="flex-1"
label="Dark Theme"
leftSlot={<Icon icon="moon" />}
leftSlot={<Icon icon="moon" color="secondary" />}
size="sm"
value={activeTheme.dark.id}
options={darkThemes}

Some files were not shown because too many files have changed in this diff Show More