first commit

This commit is contained in:
aOK 2023-12-11 23:23:39 +03:00
commit 117a0fc5ed
34 changed files with 3100 additions and 0 deletions

17
.cargo/config.toml Normal file
View File

@ -0,0 +1,17 @@
# Copyright 2019-2022 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"
rustflags = ["-C", "target-feature=-crt-static"]
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target/
Cargo.lock

24
Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[workspace]
resolver = "2"
members = [
"packages/cli",
# "packages/cli/node".
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.small]
inherits = "release"
#debug = true
opt-level = 'z' # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true

37
README.md Normal file
View File

@ -0,0 +1,37 @@
cargo run my-app
<img src="https://makepad.nl/img/logo_makepad.svg" alt="Rapidly scaffold out a new Makepad app project." />
# Usage
## Cargo:
```bash
cargo install create-makepad-app
cargo create-makepad-app
```
<br>
```bash
# cargo
cargo create-makepad-app my-app --template counter --manager cargo
```
Currently supported template presets include:
- `counter`
- `simple stack navigation`
- `simple login`
You can use `.` for the project name to scaffold in the current directory.
## Semver
**create-makepad-app** is following [Semantic Versioning 2.0](https://semver.org/).
## Licenses
Code: (c) 2022.
MIT or MIT/Apache 2.0 where applicable.

24
packages/cli/Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "create-makepad-app"
description = "Rapidly scaffold out a new makepad app project."
version = "0.1.0"
edition = "2021"
license = "Apache-2.0 OR MIT"
readme = "README.md"
repository = "https://gitdab.com/andodeki/create-makepad-app"
keywords = [ "makepad" ]
categories = [ "gui" ]
exclude = [ "/node" ]
rust-version = "1.59"
[[bin]]
name = "cargo-create-makepad-app"
path = "src/main.rs"
[dependencies]
anyhow = "1"
dialoguer = "0.10"
pico-args = "0.5"
ctrlc = "3.3"
rust-embed = { version = "8.0", features = [ "compression", "interpolate-folder-path" ] }
makepad-widgets = { version = "0.6.0" }

48
packages/cli/README.md Normal file
View File

@ -0,0 +1,48 @@
# Usage
## Cargo:
```bash
cargo install create-makepad-app
cargo create-makepad-app
```
# Clone this repository Need help cloning? Visit Help.
https://gitdab.com/andodeki/makepad-template.git
# Creating a new repository on the command line
touch README.md
git init
git checkout -b main
git add README.md
git commit -m "first commit"
git remote add origin https://gitdab.com/andodeki/makepad-template.git
git push -u origin main
# Pushing an existing repository from the command line
git remote add origin https://gitdab.com/andodeki/makepad-template.git
git push -u origin main
# Desktop in Debug Mode
cargo run
# Desktop in Release Mode
cargo run --release
# Desktop in small size
cargo run --profile=small
# Android
cargo makepad android run --release
# IOS Simulator
cargo makepad apple ios --org=my.test --app=makepad-template run-sim --release
# IOS Device
cargo makepad apple ios --org-id=123456 --org=my.test --app=makepad-template run-device makepad-template --release
# Cargo Check Builds
cargo makepad check install-toolchain
cargo makepad check all
cargo makepad wasm install-toolchain
cargo makepad apple ios install-toolchain
cargo makepad android --abi=all install-toolchain

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 7067 1035" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-54.1122,-304.466)">
<rect id="ArtBoard5" x="54.112" y="304.466" width="7066.93" height="1034.07" style="fill:none;"/>
<g>
<clipPath id="_clip1">
<rect x="54.112" y="304.466" width="7066.93" height="1034.07"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(2.46433,0,0,0.355348,0.112216,246.617)">
<g transform="matrix(0.412987,0,0,2.51071,-14964.8,-23383.5)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(-0.412987,0,0,2.51071,15723.6,-23383.5)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(0.412987,0,0,2.51071,-15087.3,-24885.4)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(-0.412987,0,0,2.51071,15846,-24885.4)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(-0.412987,-3.50746e-16,4.43366e-17,-2.51071,16084.4,28088.3)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(0.412987,-3.50746e-16,-4.43366e-17,-2.51071,-15325.6,28088.3)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
</g>
<g transform="matrix(83.3091,0,0,83.3091,-43233.8,-47386.6)">
<path d="M544.422,582.292L546.018,582.292L546.63,579.016C546.75,578.38 546.87,577.936 547.026,577.672C547.302,577.204 547.746,576.952 548.31,576.952C548.718,576.952 549.054,577.096 549.222,577.348C549.318,577.492 549.366,577.684 549.366,577.948C549.366,578.14 549.33,578.368 549.282,578.644L548.61,582.292L550.206,582.292L550.818,579.016C550.95,578.296 551.07,577.924 551.274,577.624C551.574,577.192 552.018,576.952 552.534,576.952C552.918,576.952 553.23,577.084 553.398,577.336C553.506,577.48 553.554,577.672 553.554,577.948C553.554,578.14 553.518,578.368 553.47,578.644L552.798,582.292L554.394,582.292L555.102,578.452C555.162,578.116 555.198,577.804 555.198,577.528C555.198,577.168 555.15,576.856 555.054,576.604C554.766,575.896 553.998,575.488 552.954,575.488C552.018,575.488 551.31,575.8 550.638,576.52C550.314,575.788 549.798,575.488 548.874,575.488C548.094,575.488 547.578,575.704 547.002,576.268L547.122,575.644L545.658,575.644L544.422,582.292Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M563.61,575.644L562.146,575.644L561.978,576.532C561.558,575.8 560.958,575.488 559.998,575.488C558.03,575.488 556.338,576.964 555.966,578.992C555.918,579.244 555.894,579.484 555.894,579.724C555.894,581.344 556.986,582.448 558.678,582.448C559.614,582.448 560.298,582.16 561.066,581.428L560.91,582.292L562.374,582.292L563.61,575.644ZM559.866,576.952C560.874,576.952 561.522,577.588 561.522,578.548C561.522,578.692 561.498,578.836 561.474,578.992C561.258,580.18 560.322,580.984 559.17,580.984C558.15,580.984 557.526,580.372 557.526,579.448C557.526,579.316 557.538,579.172 557.562,579.028C557.79,577.828 558.75,576.952 559.866,576.952Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M563.67,582.292L565.266,582.292L565.842,579.184L567.534,582.292L569.682,582.292L567.546,578.896L570.534,575.644L568.626,575.644L565.914,578.776L566.91,573.424L565.314,573.424L563.67,582.292Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M576.918,579.676C577.014,579.448 577.05,579.304 577.098,579.064C577.146,578.8 577.17,578.548 577.17,578.308C577.17,576.64 576.054,575.488 574.326,575.488C572.37,575.488 570.534,577.036 570.174,578.992C570.138,579.208 570.114,579.424 570.114,579.628C570.114,581.26 571.338,582.448 573.138,582.448C574.23,582.448 575.142,582.064 575.994,581.248C576.306,580.936 576.522,580.66 576.69,580.324L574.95,580.324C574.458,580.804 574.026,580.984 573.366,580.984C572.418,580.984 571.818,580.48 571.77,579.676L576.918,579.676ZM571.986,578.272C572.394,577.408 573.126,576.952 574.074,576.952C575.058,576.952 575.622,577.42 575.67,578.272L571.986,578.272Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M577.074,584.512L578.67,584.512L579.198,581.668C579.714,582.208 580.278,582.448 581.094,582.448C583.014,582.448 584.694,580.96 585.066,578.944C585.114,578.692 585.138,578.44 585.138,578.2C585.138,576.592 584.07,575.488 582.39,575.488C581.49,575.488 580.662,575.812 580.038,576.424L580.182,575.644L578.718,575.644L577.074,584.512ZM581.874,576.952C582.858,576.952 583.494,577.612 583.494,578.548C583.494,578.68 583.47,578.836 583.446,578.98C583.242,580.108 582.246,580.984 581.178,580.984C580.206,580.984 579.558,580.312 579.558,579.388C579.558,579.256 579.57,579.124 579.594,578.992C579.81,577.828 580.794,576.952 581.874,576.952Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M593.37,575.644L591.906,575.644L591.738,576.532C591.318,575.8 590.718,575.488 589.758,575.488C587.79,575.488 586.098,576.964 585.726,578.992C585.678,579.244 585.654,579.484 585.654,579.724C585.654,581.344 586.746,582.448 588.438,582.448C589.374,582.448 590.058,582.16 590.826,581.428L590.67,582.292L592.134,582.292L593.37,575.644ZM589.626,576.952C590.634,576.952 591.282,577.588 591.282,578.548C591.282,578.692 591.258,578.836 591.234,578.992C591.018,580.18 590.082,580.984 588.93,580.984C587.91,580.984 587.286,580.372 587.286,579.448C587.286,579.316 587.298,579.172 587.322,579.028C587.55,577.828 588.51,576.952 589.626,576.952Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M601.758,573.424L600.162,573.424L599.646,576.232C599.274,575.716 598.554,575.404 597.714,575.404C595.854,575.404 594.114,576.964 593.742,578.956C593.694,579.196 593.67,579.436 593.67,579.664C593.67,581.284 594.774,582.448 596.43,582.448C597.33,582.448 598.026,582.148 598.806,581.428L598.65,582.292L600.114,582.292L601.758,573.424ZM597.654,576.868C598.662,576.868 599.298,577.528 599.298,578.5C599.298,578.644 599.286,578.788 599.262,578.944C599.046,580.084 598.026,580.984 596.946,580.984C595.974,580.984 595.314,580.276 595.314,579.316C595.314,579.184 595.326,579.052 595.35,578.908C595.566,577.756 596.574,576.868 597.654,576.868Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,109 @@
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

View File

@ -0,0 +1,3 @@
/src
/public
/Cargo.toml

View File

@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

View File

@ -0,0 +1,5 @@
{
"emmet.includeLanguages": {
"rust": "html"
}
}

View File

@ -0,0 +1,25 @@
[package]
name = "{% package_name %}"
version = "0.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.small]
inherits = "release"
#debug = true
opt-level = 'z' # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true
[dependencies]
makepad-widgets = { version = "0.6.0" }

View File

@ -0,0 +1,34 @@
# Makepad UI
This template should help get you started developing with Makepad Rust UI.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
# Desktop in Debug Mode
cargo run
# Desktop in Release Mode
cargo run --release
# Desktop in small size
cargo run --profile=small
# Android
cargo makepad android run --release
# IOS Simulator
cargo makepad apple ios --org=my.test --app=makepad-template run-sim --release
# IOS Device
cargo makepad apple ios --org-id=123456 --org=my.test --app=makepad-template run-device makepad-template --release
# Cargo Check Builds
cargo makepad check install-toolchain
cargo makepad check all
cargo makepad wasm install-toolchain
cargo makepad apple ios install-toolchain
cargo makepad android --abi=all install-toolchain

View File

@ -0,0 +1,11 @@
[build]
target = "./index.html"
[watch]
ignore = ["./src-tauri"]
[serve]
address = "{% if mobile %}0.0.0.0{% else %}127.0.0.1{% endif %}"
port = 1420
open = false{% if mobile %}
ws_protocol = "ws"{% endif %}

View File

@ -0,0 +1,13 @@
# Copyright 2019-2022 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
beforeDevCommand = trunk serve
beforeBuildCommand = trunk build
devPath = http://localhost:1420
distDir = ../dist
withGlobalTauri = true
[files]
makepad.svg = resources/makepad.svg
styles.css = styles.css

View File

@ -0,0 +1,3 @@
/dist/
/target/
/Cargo.lock

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Makepad Template</title>
<script type='module'>
import {WasmWebGL} from "/{% package_name %}/platform/src/os/web/web_gl.js"
const wasm = await WasmWebGL.fetch_and_instantiate_wasm(
"/{% package_name %}/target/wasm32-unknown-unknown/release/{% package_name %}.wasm"
);
class MyWasmApp {
constructor(wasm) {
let canvas = document.getElementsByClassName('full_canvas')[0];
this.webgl = new WasmWebGL (wasm, this, canvas);
}
}
let app = new MyWasmApp(wasm);
</script>
<script type='module' src='/{% package_name %}/platform/src/os/web/auto_reload.js'></script>
<link rel='stylesheet' type='text/css' href='/{% package_name %}/platform/src/os/web/full_canvas.css'>
</head>
<body>
<canvas class='full_canvas'></canvas>
<div class='canvas_loader' >
<div style=''>
Loading..
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 7067 1035" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-54.1122,-304.466)">
<rect id="ArtBoard5" x="54.112" y="304.466" width="7066.93" height="1034.07" style="fill:none;"/>
<g>
<clipPath id="_clip1">
<rect x="54.112" y="304.466" width="7066.93" height="1034.07"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(2.46433,0,0,0.355348,0.112216,246.617)">
<g transform="matrix(0.412987,0,0,2.51071,-14964.8,-23383.5)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(-0.412987,0,0,2.51071,15723.6,-23383.5)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(0.412987,0,0,2.51071,-15087.3,-24885.4)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(-0.412987,0,0,2.51071,15846,-24885.4)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(-0.412987,-3.50746e-16,4.43366e-17,-2.51071,16084.4,28088.3)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
<g transform="matrix(0.412987,-3.50746e-16,-4.43366e-17,-2.51071,-15325.6,28088.3)">
<path d="M37739.3,9977.06L38019.7,10537.9L37458.9,10537.9L37739.3,9977.06Z" style="fill:rgb(255,92,57);"/>
</g>
</g>
<g transform="matrix(83.3091,0,0,83.3091,-43233.8,-47386.6)">
<path d="M544.422,582.292L546.018,582.292L546.63,579.016C546.75,578.38 546.87,577.936 547.026,577.672C547.302,577.204 547.746,576.952 548.31,576.952C548.718,576.952 549.054,577.096 549.222,577.348C549.318,577.492 549.366,577.684 549.366,577.948C549.366,578.14 549.33,578.368 549.282,578.644L548.61,582.292L550.206,582.292L550.818,579.016C550.95,578.296 551.07,577.924 551.274,577.624C551.574,577.192 552.018,576.952 552.534,576.952C552.918,576.952 553.23,577.084 553.398,577.336C553.506,577.48 553.554,577.672 553.554,577.948C553.554,578.14 553.518,578.368 553.47,578.644L552.798,582.292L554.394,582.292L555.102,578.452C555.162,578.116 555.198,577.804 555.198,577.528C555.198,577.168 555.15,576.856 555.054,576.604C554.766,575.896 553.998,575.488 552.954,575.488C552.018,575.488 551.31,575.8 550.638,576.52C550.314,575.788 549.798,575.488 548.874,575.488C548.094,575.488 547.578,575.704 547.002,576.268L547.122,575.644L545.658,575.644L544.422,582.292Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M563.61,575.644L562.146,575.644L561.978,576.532C561.558,575.8 560.958,575.488 559.998,575.488C558.03,575.488 556.338,576.964 555.966,578.992C555.918,579.244 555.894,579.484 555.894,579.724C555.894,581.344 556.986,582.448 558.678,582.448C559.614,582.448 560.298,582.16 561.066,581.428L560.91,582.292L562.374,582.292L563.61,575.644ZM559.866,576.952C560.874,576.952 561.522,577.588 561.522,578.548C561.522,578.692 561.498,578.836 561.474,578.992C561.258,580.18 560.322,580.984 559.17,580.984C558.15,580.984 557.526,580.372 557.526,579.448C557.526,579.316 557.538,579.172 557.562,579.028C557.79,577.828 558.75,576.952 559.866,576.952Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M563.67,582.292L565.266,582.292L565.842,579.184L567.534,582.292L569.682,582.292L567.546,578.896L570.534,575.644L568.626,575.644L565.914,578.776L566.91,573.424L565.314,573.424L563.67,582.292Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M576.918,579.676C577.014,579.448 577.05,579.304 577.098,579.064C577.146,578.8 577.17,578.548 577.17,578.308C577.17,576.64 576.054,575.488 574.326,575.488C572.37,575.488 570.534,577.036 570.174,578.992C570.138,579.208 570.114,579.424 570.114,579.628C570.114,581.26 571.338,582.448 573.138,582.448C574.23,582.448 575.142,582.064 575.994,581.248C576.306,580.936 576.522,580.66 576.69,580.324L574.95,580.324C574.458,580.804 574.026,580.984 573.366,580.984C572.418,580.984 571.818,580.48 571.77,579.676L576.918,579.676ZM571.986,578.272C572.394,577.408 573.126,576.952 574.074,576.952C575.058,576.952 575.622,577.42 575.67,578.272L571.986,578.272Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M577.074,584.512L578.67,584.512L579.198,581.668C579.714,582.208 580.278,582.448 581.094,582.448C583.014,582.448 584.694,580.96 585.066,578.944C585.114,578.692 585.138,578.44 585.138,578.2C585.138,576.592 584.07,575.488 582.39,575.488C581.49,575.488 580.662,575.812 580.038,576.424L580.182,575.644L578.718,575.644L577.074,584.512ZM581.874,576.952C582.858,576.952 583.494,577.612 583.494,578.548C583.494,578.68 583.47,578.836 583.446,578.98C583.242,580.108 582.246,580.984 581.178,580.984C580.206,580.984 579.558,580.312 579.558,579.388C579.558,579.256 579.57,579.124 579.594,578.992C579.81,577.828 580.794,576.952 581.874,576.952Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M593.37,575.644L591.906,575.644L591.738,576.532C591.318,575.8 590.718,575.488 589.758,575.488C587.79,575.488 586.098,576.964 585.726,578.992C585.678,579.244 585.654,579.484 585.654,579.724C585.654,581.344 586.746,582.448 588.438,582.448C589.374,582.448 590.058,582.16 590.826,581.428L590.67,582.292L592.134,582.292L593.37,575.644ZM589.626,576.952C590.634,576.952 591.282,577.588 591.282,578.548C591.282,578.692 591.258,578.836 591.234,578.992C591.018,580.18 590.082,580.984 588.93,580.984C587.91,580.984 587.286,580.372 587.286,579.448C587.286,579.316 587.298,579.172 587.322,579.028C587.55,577.828 588.51,576.952 589.626,576.952Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
<path d="M601.758,573.424L600.162,573.424L599.646,576.232C599.274,575.716 598.554,575.404 597.714,575.404C595.854,575.404 594.114,576.964 593.742,578.956C593.694,579.196 593.67,579.436 593.67,579.664C593.67,581.284 594.774,582.448 596.43,582.448C597.33,582.448 598.026,582.148 598.806,581.428L598.65,582.292L600.114,582.292L601.758,573.424ZM597.654,576.868C598.662,576.868 599.298,577.528 599.298,578.5C599.298,578.644 599.286,578.788 599.262,578.944C599.046,580.084 598.026,580.984 596.946,580.984C595.974,580.984 595.314,580.276 595.314,579.316C595.314,579.184 595.326,579.052 595.35,578.908C595.566,577.756 596.574,576.868 597.654,576.868Z" style="fill:rgb(250,225,188);fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,91 @@
use makepad_widgets::*;
live_design!{
import makepad_widgets::base::*;
import makepad_widgets::theme_desktop_dark::*;
import crate::shared::styles::*;
LOGO_MAKEPAD = dep("crate://self/resources/makepad.svg")
App = {{App}} {
ui: <Window>{
show_bg: true
width: Fill,
height: Fill
draw_bg: {
fn pixel(self) -> vec4 {
//return #000
return mix(#7, #3, self.pos.y);
}
}
body = <View>{
flow: Down,
spacing: 20,
align: {
x: 0.5,
y: 0.5
},
avatar = <Image> {
source: (LOGO_MAKEPAD),
width: 50., height: 50.
}
button1 = <Button> {
text: "Hello world"
}
input1 = <TextInput> {
width: 100, height: 30
text: "Click to count"
}
label1 = <Label> {
draw_text: {
color: #f
},
text: "Counter: 0"
}
}
}
}
}
app_main!(App);
#[derive(Live)]
pub struct App {
#[live] ui: WidgetRef,
#[rust] counter: usize,
}
impl LiveHook for App {
fn before_live_design(cx: &mut Cx) {
crate::makepad_widgets::live_design(cx);
}
}
impl App{
async fn _do_network_request(_cx:CxRef, _ui:WidgetRef, _url:&str)->String{
"".to_string()
}
}
impl AppMain for App{
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
if let Event::Draw(event) = event {
return self.ui.draw_widget_all(&mut Cx2d::new(cx, event));
}
let actions = self.ui.handle_widget_event(cx, event);
if self.ui.button(id!(button1)).clicked(&actions) {
log!("BUTTON CLICKED {}", self.counter);
self.counter += 1;
let label = self.ui.label(id!(label1));
label.set_text_and_redraw(cx,&format!("Counter: {}", self.counter));
}
}
}

View File

@ -0,0 +1,2 @@
pub use makepad_widgets;
pub mod app;

View File

@ -0,0 +1,6 @@
// this stub is necessary because some platforms require building
// as dll (mobile / wasm) and some require to be built as executable
// unfortunately cargo doesn't facilitate this without a main.rs stub
fn main(){
{% package_name %}::app::app_main()
}

View File

@ -0,0 +1 @@
pub mod styles;

View File

@ -0,0 +1,22 @@
use makepad_widgets::*;
live_design! {
TITLE_TEXT = {
font_size: (14),
font: {path: dep("crate://makepad-widgets/resources/GoNotoKurrent-Regular.ttf")}
}
REGULAR_TEXT = {
font_size: (12),
font: {path: dep("crate://makepad-widgets/resources/GoNotoKurrent-Regular.ttf")}
}
TEXT_SUB = {
font_size: (FONT_SIZE_SUB),
font: {path: dep("crate://makepad-widgets/resources/GoNotoKurrent-Regular.ttf")}
}
COLOR_PROFILE_CIRCLE = #xfff8ee
COLOR_DIVIDER = #x00000018
COLOR_DIVIDER_DARK = #x00000044
}

View File

@ -0,0 +1,40 @@
use std::fmt::Display;
use crate::package_manager::PackageManager;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Category {
Rust,
}
impl Default for Category {
fn default() -> Self {
Category::Rust
}
}
impl<'a> Category {
pub const ALL: &'a [Self] = &[Category::Rust];
pub const fn package_managers(&self) -> &[PackageManager] {
match self {
Category::Rust => &[PackageManager::Cargo],
}
}
}
impl Display for Category {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let managers = self
.package_managers()
.to_vec()
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(", ");
match self {
Category::Rust => write!(f, "Rust - ({managers})"),
// Category::JsTs => write!(f, "TypeScript / JavaScript - ({managers})"),
}
}
}

93
packages/cli/src/cli.rs Normal file
View File

@ -0,0 +1,93 @@
use std::ffi::OsString;
use pico_args::Arguments;
use crate::{colors::*, package_manager::PackageManager, template::Template};
#[derive(Debug)]
pub struct Args {
pub project_name: Option<String>,
pub manager: Option<PackageManager>,
pub template: Option<Template>,
pub skip: bool,
pub alpha: bool,
pub mobile: Option<bool>,
}
impl Default for Args {
fn default() -> Self {
Self {
project_name: Some("makepad-app".to_string()),
manager: Some(PackageManager::Cargo),
template: Some(Template::MakepadCounter),
skip: false,
alpha: false,
mobile: Some(false),
}
}
}
pub fn parse(argv: Vec<OsString>, bin_name: Option<String>) -> anyhow::Result<Args> {
let mut pargs = Arguments::from_vec(argv);
if pargs.contains(["-h", "--help"]) {
let help = format!(
r#"
{GREEN}{name}{RESET} {version}
{authors}
{desc}
{YELLOW}USAGE:{RESET}
{name} [OPTIONS] [PROJECTNAME]
{YELLOW}ARGS:{RESET}
{GREEN}<PROJECTNAME>{RESET} Specify project name which is used for the directory, package.json and Cargo.toml
{YELLOW}OPTIONS:{RESET}
{GREEN}-m{RESET}, {GREEN}--manager <MANAGER>{RESET} Specify preferred package manager [{managers}]
{GREEN}-t{RESET}, {GREEN}--template <TEMPLATE>{RESET} Specify the UI template to use [{fragments}]
{GREEN}-y{RESET}, {GREEN}--yes{RESET} Skip prompts and use defaults where applicable
{GREEN}--alpha{RESET} Bootstraps a project using tauri@2.0-alpha
{GREEN}--mobile{RESET} Bootstraps a mobile project too. Only availabe with `--alpha` option.
{GREEN}-h{RESET}, {GREEN}--help{RESET} Prints help information
{GREEN}-v{RESET}, {GREEN}--version{RESET} Prints version information
"#,
name = bin_name.unwrap_or_else(|| env!("CARGO_PKG_NAME").to_string()),
version = env!("CARGO_PKG_VERSION"),
authors = env!("CARGO_PKG_AUTHORS"),
desc = env!("CARGO_PKG_DESCRIPTION"),
managers = PackageManager::ALL
.iter()
.map(|e| format!("{GREEN}{e}{RESET}"))
.collect::<Vec<_>>()
.join(", "),
fragments = Template::ALL
.iter()
.map(|e| format!("{GREEN}{e}{RESET}"))
.collect::<Vec<_>>()
.join(", "),
);
println!("{help}");
std::process::exit(0);
}
if pargs.contains(["-v", "--version"]) {
println!("{}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
let args = Args {
manager: pargs.opt_value_from_str(["-m", "--manager"])?,
template: pargs.opt_value_from_str(["-t", "--template"])?,
skip: pargs.contains(["-y", "--yes"]),
alpha: pargs.contains("--alpha"),
mobile: if pargs.contains("--mobile") {
Some(true)
} else {
None
},
project_name: pargs.opt_free_from_str()?,
};
Ok(args)
}

View File

@ -0,0 +1,27 @@
#![allow(unused)]
pub const BLACK: &str = "\x1b[30m";
pub const RED: &str = "\x1b[31m";
pub const GREEN: &str = "\x1b[32m";
pub const YELLOW: &str = "\x1b[33m";
pub const BLUE: &str = "\x1b[34m";
pub const WHITE: &str = "\x1b[37m";
pub const RESET: &str = "\x1b[0m";
pub const BOLD: &str = "\x1b[1m";
pub const ITALIC: &str = "\x1b[3m";
pub const DIM: &str = "\x1b[2m";
pub const DIMRESET: &str = "\x1b[22m";
pub fn remove_colors(s: &str) -> String {
s.replace(BLACK, "")
.replace(RED, "")
.replace(GREEN, "")
.replace(YELLOW, "")
.replace(BLUE, "")
.replace(WHITE, "")
.replace(RESET, "")
.replace(BOLD, "")
.replace(ITALIC, "")
.replace(DIM, "")
.replace(DIMRESET, "")
}

324
packages/cli/src/deps.rs Normal file
View File

@ -0,0 +1,324 @@
use template::Template;
use crate::colors::*;
use crate::internal::template;
use crate::package_manager::PackageManager;
use std::process::{Command, Output};
fn is_rustc_installed() -> bool {
Command::new("rustc")
.arg("-V")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn is_cargo_installed() -> bool {
Command::new("cargo")
.arg("-V")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn is_node_installed() -> bool {
Command::new("node")
.arg("-v")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn is_trunk_installed() -> bool {
Command::new("trunk")
.arg("-V")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn is_appropriate_makepad_cli_installed(alpha: bool) -> bool {
let check = |o: Output| match o.status.success() {
true if alpha => String::from_utf8_lossy(&o.stderr)
.split_once(' ')
.map(|(_, v)| v.starts_with('2'))
.unwrap_or(false),
s => s,
};
Command::new("cargo")
.args(["makepad", "-V"])
.output()
.map(check)
.or_else(|_| Command::new("makepad").arg("-V").output().map(check))
.unwrap_or(false)
}
fn is_wasm32_installed() -> bool {
Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
.map(|o| {
let s = String::from_utf8_lossy(&o.stdout);
s.contains("wasm32-unknown-unknown")
})
.unwrap_or(false)
}
#[cfg(windows)]
fn is_webview2_installed() -> bool {
let powershell_path = std::env::var("SYSTEMROOT").map_or_else(
|_| "powershell.exe".to_string(),
|p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
);
// check 64bit per-system installation
let output = Command::new(&powershell_path)
.args(["-NoProfile", "-Command"])
.arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
.output().map(|o|o.status.success());
if let Ok(o) = output {
if o {
return true;
}
}
// check 32bit per-system installation
let output = Command::new(&powershell_path)
.args(["-NoProfile", "-Command"])
.arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
.output().map(|o|o.status.success());
if let Ok(o) = output {
if o {
return true;
}
}
// check per-user installation
let output = Command::new(&powershell_path)
.args(["-NoProfile", "-Command"])
.arg("Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
.output().map(|o|o.status.success());
if let Ok(o) = output {
if o {
return true;
}
}
false
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd"
))]
fn is_webkit2gtk_installed(alpha: bool) -> bool {
Command::new("pkg-config")
.arg(if alpha {
"webkit2gtk-4.1"
} else {
"webkit2gtk-4.0"
})
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd"
))]
fn is_rsvg2_installed() -> bool {
Command::new("pkg-config")
.arg("librsvg-2.0")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
fn is_xcode_command_line_tools_installed() -> bool {
Command::new("xcode-select")
.arg("-p")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn print_missing_deps(pkg_manager: PackageManager, template: Template, alpha: bool) {
let rustc_installed = is_rustc_installed();
let cargo_installed = is_cargo_installed();
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd"
))]
let (webkit2gtk_installed, rsvg2_installed) =
(is_webkit2gtk_installed(alpha), is_rsvg2_installed());
let deps: &[(&str, String, &dyn Fn() -> bool, bool)] = &[
(
"Rust",
format!("Visit {BLUE}{BOLD}https://www.rust-lang.org/learn/get-started#installing-rust{RESET}"),
&|| rustc_installed && cargo_installed,
rustc_installed || cargo_installed,
),
(
"rustc",
format!("Visit {BLUE}{BOLD}https://www.rust-lang.org/learn/get-started#installing-rust{RESET} to install Rust"),
&|| rustc_installed,
!rustc_installed && !cargo_installed,
),
(
"Cargo",
format!("Visit {BLUE}{BOLD}https://www.rust-lang.org/learn/get-started#installing-rust{RESET} to install Rust"),
&|| cargo_installed,
!rustc_installed && !cargo_installed,
),
(
"Makepad CLI",
if alpha {
format!("Run `{BLUE}{BOLD}cargo install makepad-cli --version '^2.0.0-alpha'{RESET}`")
} else {
format!("Run `{BLUE}{BOLD}cargo install makepad-cli{RESET}`")
},
&|| is_appropriate_makepad_cli_installed(alpha),
!template.needs_makepad_cli(),
),
(
"Trunk",
if alpha {
format!("Run `{BLUE}{BOLD}cargo install trunk --git https://github.com/amrbashir/trunk{RESET}`")
} else {
format!("Run `{BLUE}{BOLD}cargo install trunk{RESET}`")
},
&is_trunk_installed,
!template.needs_trunk(),
),
(
"wasm32 target",
format!("Run `{BLUE}{BOLD}rustup target add wasm32-unknown-unknown{RESET}`"),
&is_wasm32_installed,
!template.needs_wasm32_target(),
),
#[cfg(windows)]
(
"Webview2",
format!("Visit {BLUE}{BOLD}https://go.microsoft.com/fwlink/p/?LinkId=2124703{RESET}"),
&is_webview2_installed,
false,
),
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd"
))]
(
"webkit2gtk & rsvg2",
format!("Visit {BLUE}{BOLD}{}{RESET}", if alpha {
"https://next--tauri.netlify.app/next/guides/getting-started/prerequisites/linux#1-system-dependencies"
} else {
"https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-linux"
}),
&|| webkit2gtk_installed && rsvg2_installed,
webkit2gtk_installed || rsvg2_installed,
),
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd"
))]
(
"webkit2gtk",
format!("Visit {BLUE}{BOLD}{}{RESET}", if alpha {
"https://next--tauri.netlify.app/next/guides/getting-started/prerequisites/linux#1-system-dependencies"
} else {
"https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-linux"
}),
&|| webkit2gtk_installed,
!rsvg2_installed && !webkit2gtk_installed,
),
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd"
))]
(
"rsvg2",
format!("Visit {BLUE}{BOLD}{}{RESET}", if alpha {
"https://next--tauri.netlify.app/next/guides/getting-started/prerequisites/linux#1-system-dependencies"
} else {
"https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-linux"
}),
&|| rsvg2_installed,
!rsvg2_installed && !webkit2gtk_installed,
),
#[cfg(target_os = "macos")]
(
"Xcode Command Line Tools",
format!("Run `{BLUE}{BOLD}xcode-select --install{RESET}`"),
&is_xcode_command_line_tools_installed,
false,
),
];
let missing_deps: Vec<(String, String)> = deps
.iter()
.filter(|(_, _, exists, skip)| !skip && !exists())
.map(|(s, d, _, _)| (s.to_string(), d.clone()))
.collect();
let (largest_first_cell, largest_second_cell) =
missing_deps
.iter()
.fold((0, 0), |(mut prev_f, mut prev_s), (f, s)| {
let f_len = f.len();
if f_len > prev_f {
prev_f = f_len;
}
let s_len = remove_colors(s).len();
if s_len > prev_s {
prev_s = s_len;
}
(prev_f, prev_s)
});
if !missing_deps.is_empty() {
println!("\n\nYour system is {YELLOW}missing dependencies{RESET} (or they do not exist in {YELLOW}$PATH{RESET}):");
for (index, (name, instruction)) in missing_deps.iter().enumerate() {
if index == 0 {
println!(
"╭{}┬{}╮",
"".repeat(largest_first_cell + 2),
"".repeat(largest_second_cell + 2)
);
} else {
println!(
"├{}┼{}┤",
"".repeat(largest_first_cell + 2),
"".repeat(largest_second_cell + 2)
);
}
println!(
"│ {YELLOW}{name}{RESET}{} │ {instruction}{} │",
" ".repeat(largest_first_cell - name.len()),
" ".repeat(largest_second_cell - remove_colors(instruction).len()),
);
}
println!(
"╰{}┴{}╯",
"".repeat(largest_first_cell + 2),
"".repeat(largest_second_cell + 2),
);
println!();
println!("Make sure you have installed the prerequisites for your OS: {BLUE}{BOLD}https://tauri.app/v1/guides/getting-started/prerequisites{RESET}, then run:");
} else {
println!(" To get started run:")
}
}

427
packages/cli/src/lib.rs Normal file
View File

@ -0,0 +1,427 @@
use anyhow::Context;
use dialoguer::{Confirm, Input, Select};
use std::{ffi::OsString, fs, process::exit};
use crate::{
category::Category,
colors::*,
deps::print_missing_deps,
package_manager::PackageManager,
theme::ColorfulTheme,
};
mod category;
mod cli;
mod colors;
mod deps;
mod lte;
mod manifest;
mod package_manager;
mod template;
mod theme;
pub mod internal {
//! Re-export of create-tauri-app internals
//!
//! ## Warning
//!
//! This is meant to be used internally only so use at your own risk
//! and expect APIs to break without a prior notice.
pub mod package_manager {
pub use crate::package_manager::*;
}
pub mod template {
pub use crate::template::*;
}
}
pub fn run<I, A>(args: I, bin_name: Option<String>, detected_manager: Option<String>)
where
I: IntoIterator<Item = A>,
A: Into<OsString> + Clone,
{
let _ = ctrlc::set_handler(move || {
eprint!("\x1b[?25h");
exit(0);
});
if let Err(e) = try_run(args, bin_name, detected_manager) {
eprintln!("{BOLD}{RED}error{RESET}: {e:#}");
exit(1);
}
}
fn try_run<I, A>(
args: I,
bin_name: Option<String>,
detected_manager: Option<String>,
) -> anyhow::Result<()>
where
I: IntoIterator<Item = A>,
A: Into<OsString> + Clone,
{
let detected_manager = detected_manager.and_then(|p| p.parse::<PackageManager>().ok());
let args = cli::parse(args.into_iter().map(Into::into).collect(), bin_name)?;
let defaults = cli::Args::default();
let cli::Args {
skip,
mobile,
alpha,
manager,
project_name,
template,
} = args;
let cwd = std::env::current_dir()?;
// Allow `--mobile` to only be used with `--alpha` for now
// TODO: remove this limitation once tauri@v2 is stable
if let Some(mobile) = mobile {
if mobile && !alpha {
eprintln!(
"{BOLD}{RED}error{RESET}: `{GREEN}--mobile{RESET}` option is only available if `{GREEN}--alpha{RESET}` option is also used"
);
exit(1);
}
}
// Project name used for the project directory name and productName in tauri.conf.json
// and if valid, it will also be used in Cargo.toml, Package.json ...etc
let project_name = match project_name {
Some(name) => name,
None => {
if skip {
defaults
.project_name
.context("default project_name not set")?
} else {
Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("Project name")
.default("tauri-app".into())
.interact_text()?
.trim()
.into()
}
}
};
let target_dir = cwd.join(&project_name);
// Package name used in Cargo.toml, Package.json ...etc
let package_name = if is_valid_pkg_name(&project_name) {
project_name.clone()
} else {
let valid_name = to_valid_pkg_name(&project_name);
if skip {
valid_name
} else {
Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("Package name")
.default(valid_name.clone())
.with_initial_text(valid_name)
.validate_with(|input: &String| {
if is_valid_pkg_name(input) {
Ok(())
} else {
Err("Package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers")
}
})
.interact_text()?
.trim().to_string()
}
};
// Confirm deleting the target project directory if not empty
if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
let overrwite = if skip {
false
} else {
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(format!(
"{} directory is not empty, do you want to overwrite?",
if target_dir == cwd {
"Current directory".to_string()
} else {
target_dir
.file_name()
.unwrap()
.to_string_lossy()
.to_string()
}
))
.default(false)
.interact()?
};
if !overrwite {
eprintln!("{BOLD}{RED}{RESET} Operation Cancelled");
exit(1);
}
};
// Prompt for category if a package manger is not passed on the command line
let category = if manager.is_none() && !skip {
// Filter managers if a template is passed on the command line
let managers = PackageManager::ALL.to_vec();
let managers = args
.template
.map(|t| {
managers
.iter()
.copied()
.filter(|p| p.templates_no_flavors().contains(&t.without_flavor()))
.collect::<Vec<_>>()
})
.unwrap_or(managers);
// Filter categories based on the detected package mangers
let categories = Category::ALL.to_vec();
let mut categories = categories
.into_iter()
.filter(|c| c.package_managers().iter().any(|p| managers.contains(p)))
.collect::<Vec<_>>();
// sort categories so the most relevant category
// based on the auto-detected package manager is selected first
categories.sort_by(|a, b| {
detected_manager
.map(|p| b.package_managers().contains(&p))
.unwrap_or(false)
.cmp(
&detected_manager
.map(|p| a.package_managers().contains(&p))
.unwrap_or(false),
)
});
// If only one category is detected, skip prompt
if categories.len() == 1 {
Some(categories[0])
} else {
let index = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose which language to use for your frontend")
.items(&categories)
.default(0)
.interact()?;
Some(categories[index])
}
} else {
None
};
// Package manager which will be used for rendering the template
// and the after-render instructions
let pkg_manager = match manager {
Some(manager) => manager,
None => {
if skip {
defaults.manager.context("default manager not set")?
} else {
let category = category.context("category shouldn't be None at this point")?;
let mut managers = category.package_managers().to_owned();
// sort managers so the auto-detected package manager is selected first
managers.sort_by(|a, b| {
detected_manager
.map(|p| p == *b)
.unwrap_or(false)
.cmp(&detected_manager.map(|p| p == *a).unwrap_or(false))
});
// If only one package manager is detected, skip prompt
if managers.len() == 1 {
managers[0]
} else {
let index = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose your package manager")
.items(&managers)
.default(0)
.interact()?;
managers[index]
}
}
}
};
let templates_no_flavors = pkg_manager.templates_no_flavors();
// Template to render
let template = match template {
Some(template) => template,
None => {
if skip {
defaults.template.context("default template not set")?
} else {
let index = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose your UI template")
.items(
&templates_no_flavors
.iter()
.map(|t| t.select_text())
.collect::<Vec<_>>(),
)
.default(0)
.interact()?;
let template = templates_no_flavors[index];
// Prompt for flavors if the template has more than one flavor
let flavors = template.flavors(pkg_manager);
if let Some(flavors) = flavors {
let index = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose your UI flavor")
.items(flavors)
.default(0)
.interact()?;
template.from_flavor(flavors[index])
} else {
template
}
}
}
};
// Prompt for wether to bootstrap a mobile-friendly tauri project
// This should only be prompted if `--alpha` is used on the command line and `--mobile` wasn't.
// TODO: remove this limitation once tauri@v2 is stable
let mobile = match mobile {
Some(mobile) => mobile,
None => {
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Would you like to setup the project for mobile as well?")
.default(false)
.interact()?
// if skip || !alpha {
// defaults.mobile.context("default mobile not set")?
// } else {
// Confirm::with_theme(&ColorfulTheme::default())
// .with_prompt("Would you like to setup the project for mobile as well?")
// .default(false)
// .interact()?
// }
}
};
// If the package manager and the template are specified on the command line
// then almost all prompts are skipped so we need to make sure that the combination
// is valid, otherwise, we error and exit
if !pkg_manager.templates().contains(&template) {
eprintln!(
"{BOLD}{RED}error{RESET}: the {GREEN}{template}{RESET} template is not suppported for the {GREEN}{pkg_manager}{RESET} package manager\n possible templates for {GREEN}{pkg_manager}{RESET} are: [{}]\n or maybe you meant to use another package manager\n possible package managers for {GREEN}{template}{RESET} are: [{}]" ,
templates_no_flavors.iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::<Vec<_>>().join(", "),
template.possible_package_managers().iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::<Vec<_>>().join(", "),
);
exit(1);
}
// Remove the target dir contents before rendering the template
// SAFETY: Upon reaching this line, the user already accepted to overwrite
if target_dir.exists() {
#[inline(always)]
fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> {
for entry in fs::read_dir(dir)?.flatten() {
let path = entry.path();
if entry.file_type()?.is_dir() {
if entry.file_name() != ".git" {
clean_dir(&path)?;
std::fs::remove_dir(path)?;
}
} else {
fs::remove_file(path)?;
}
}
Ok(())
}
clean_dir(&target_dir)?;
} else {
let _ = fs::create_dir_all(&target_dir);
}
// Render the template
template.render(
&target_dir,
pkg_manager,
&project_name,
&package_name,
alpha,
mobile,
)?;
// Print post-render instructions
println!();
print!("Template created!");
print_missing_deps(pkg_manager, template, alpha);
if target_dir != cwd {
println!(
" cd {}",
if project_name.contains(' ') {
format!("\"{project_name}\"")
} else {
project_name.clone()
}
);
}
if let Some(cmd) = pkg_manager.install_cmd() {
println!(" {cmd}");
}
if !mobile {
println!(" {} run", pkg_manager.run_cmd());
println!();
println!("For Release, run:");
println!(" {} run --release", pkg_manager.run_cmd());
println!();
println!("For Small Release, run:");
println!(" {} run --profile=small", pkg_manager.run_cmd());
} else {
println!(" {} makepad android --abi=all install-toolchain", pkg_manager.run_cmd());
#[cfg(target_os = "macos")]
println!(" {} makepad apple ios install-toolchain", pkg_manager.run_cmd());
println!();
println!("For Desktop development, run:");
println!(" {} run", pkg_manager.run_cmd());
println!();
println!("For Release, run:");
println!(" {} run --release", pkg_manager.run_cmd());
println!();
println!("For Small Release, run:");
println!(" {} run --profile=small", pkg_manager.run_cmd());
println!();
println!("For Android development, run:");
println!(" {} makepad android run", pkg_manager.run_cmd());
#[cfg(target_os = "macos")]
{
println!();
println!("For iOS Simulator development, run:");
println!(" {} makepad --org-id=123456 --org={}.com --app={} run-sim --release", pkg_manager.run_cmd(), project_name, project_name);
println!();
println!("For iOS Device development, run:");
println!(" {} makepad --org-id=123456 --org={}.com --app={} run-device {}", pkg_manager.run_cmd(), project_name, project_name, project_name);
}
}
println!();
Ok(())
}
fn is_valid_pkg_name(project_name: &str) -> bool {
let mut chars = project_name.chars().peekable();
!project_name.is_empty()
&& !chars.peek().map(|c| c.is_ascii_digit()).unwrap_or_default()
&& !chars.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '_') || ch.is_uppercase())
}
fn to_valid_pkg_name(project_name: &str) -> String {
let ret = project_name
.trim()
.to_lowercase()
.replace([':', ';', ' ', '~'], "-")
.replace(['.', '\\', '/'], "");
let ret = ret
.chars()
.skip_while(|ch| ch.is_ascii_digit() || *ch == '-')
.collect::<String>();
if ret.is_empty() {
"tauri-app".to_string()
} else {
ret
}
}

492
packages/cli/src/lte.rs Normal file
View File

@ -0,0 +1,492 @@
// why was this module named `lte`? short for "Light Template Engine" and might be extracted into its own crate.
// TODOs:
// 1. negative boolean
// 2. multiple condition in the same if
// 3. equality checks
// 4. infinte loop when missing a closing `%}`
use std::{collections::HashMap, fmt::Display};
#[derive(Debug)]
pub struct TemplateParseError {
message: String,
}
impl TemplateParseError {
fn new(message: String) -> Self {
Self { message }
}
}
impl std::fmt::Display for TemplateParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to parse template: {}", self.message)
}
}
impl std::error::Error for TemplateParseError {}
type Result<T> = std::result::Result<T, TemplateParseError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Token<'a> {
OBracket,
If,
Var(&'a str),
Else,
EndIf,
CBracket,
Text(&'a str),
Invalid(usize),
}
impl Display for Token<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Token::OBracket => write!(f, "{{%"),
Token::If => write!(f, "if"),
Token::Var(var) => write!(f, "{} (variable)", var),
Token::Else => write!(f, "else"),
Token::EndIf => write!(f, "endif"),
Token::CBracket => write!(f, "%}}"),
Token::Text(_) => write!(f, "(text)"),
Token::Invalid(col) => write!(f, "invalid token at {col}"),
}
}
}
const KEYWORDS: &[(&str, Token)] = &[
("if", Token::If),
("else", Token::Else),
("endif", Token::EndIf),
];
struct Lexer<'a> {
input: &'a str,
bytes: &'a [u8],
len: usize,
cursor: usize,
in_bracket: bool,
}
impl<'a> Lexer<'a> {
fn new(input: &'a str) -> Self {
let bytes = input.as_bytes();
let len = bytes.len();
Self {
len,
bytes,
input,
cursor: 0,
in_bracket: false,
}
}
fn current_char(&self) -> char {
self.bytes[self.cursor] as char
}
fn next_char(&self) -> char {
self.bytes[self.cursor + 1] as char
}
fn skip_whitespace(&mut self) {
while self.cursor < self.len && self.current_char().is_whitespace() {
self.cursor += 1;
}
}
fn is_symbol_start(&self) -> bool {
let c = self.current_char();
c.is_alphabetic() || c == '_'
}
fn is_symbol(&self) -> bool {
let c = self.current_char();
c.is_alphanumeric() || c == '_'
}
fn read_symbol(&mut self) -> &'a str {
let start = self.cursor;
while self.is_symbol() {
self.cursor += 1;
}
let end = self.cursor - 1;
&self.input[start..=end]
}
fn next(&mut self) -> Option<Token<'a>> {
if self.in_bracket {
self.skip_whitespace();
}
if self.cursor >= self.len {
return None;
}
if self.current_char() == '{' && self.next_char() == '%' {
self.in_bracket = true;
self.cursor += 2;
return Some(Token::OBracket);
}
if self.current_char() == '%' && self.next_char() == '}' {
self.in_bracket = false;
self.cursor += 2;
return Some(Token::CBracket);
}
if self.in_bracket {
if self.is_symbol_start() {
let symbol = self.read_symbol();
for (keyword, t) in KEYWORDS {
if *keyword == symbol {
return Some(*t);
}
}
return Some(Token::Var(symbol));
} else {
self.cursor += 1;
return Some(Token::Invalid(self.cursor));
}
}
if !self.in_bracket {
let start = self.cursor;
while !(self.current_char() == '{' && self.next_char() == '%') {
self.cursor += 1;
if self.cursor >= self.len {
break;
}
}
let end = self.cursor - 1;
return Some(Token::Text(&self.input[start..=end]));
}
None
}
}
impl<'a> Iterator for Lexer<'a> {
type Item = Token<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.next()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Stmt<'a> {
Text(&'a str),
Var(&'a str),
If {
var: &'a str,
condition: Vec<Stmt<'a>>,
else_condition: Option<Vec<Stmt<'a>>>,
},
}
impl<'a> Stmt<'a> {
fn execute<T: ToString>(&self, out: &mut String, data: &HashMap<&str, T>) -> Result<()> {
match self {
Stmt::Text(t) => out.push_str(t),
Stmt::Var(v) => {
let value = data.get(v).ok_or_else(|| {
TemplateParseError::new(format!("Unrecognized variable: {v}"))
})?;
out.push_str(&value.to_string())
}
Stmt::If {
var,
condition,
else_condition,
} => {
let value = data
.get(var)
.ok_or_else(|| {
TemplateParseError::new(format!("Unrecognized variable: {var}"))
})?
.to_string();
let evaluated = if value == "true" {
condition.as_slice()
} else if let Some(else_condition) = else_condition {
else_condition.as_slice()
} else {
&[]
};
for stmt in evaluated {
stmt.execute(out, data)?;
}
}
}
Ok(())
}
}
struct Parser<'a> {
tokens: &'a [Token<'a>],
len: usize,
cursor: usize,
}
impl<'a> Parser<'a> {
fn new(tokens: &'a [Token<'a>]) -> Self {
Self {
len: tokens.len(),
tokens,
cursor: 0,
}
}
fn current_token(&self) -> Token<'a> {
self.tokens[self.cursor]
}
fn skip_brackets(&mut self) {
if self.cursor < self.len {
while self.current_token() == Token::OBracket || self.current_token() == Token::CBracket
{
self.cursor += 1;
if self.cursor >= self.len {
break;
}
}
}
}
fn consume_text(&mut self) -> Option<&'a str> {
if let Token::Text(text) = self.current_token() {
self.cursor += 1;
Some(text)
} else {
None
}
}
fn consume_var(&mut self) -> Option<&'a str> {
if let Token::Var(var) = self.current_token() {
self.cursor += 1;
Some(var)
} else {
None
}
}
fn consume_if(&mut self) -> Result<Option<Stmt<'a>>> {
if self.current_token() == Token::If {
self.cursor += 1;
let var = self.consume_var().ok_or_else(|| {
TemplateParseError::new(format!(
"expected variable after if, found: {}",
self.current_token()
))
})?;
let mut condition = Vec::new();
while self.current_token() != Token::Else || self.current_token() != Token::EndIf {
match self.next()? {
Some(stmt) => condition.push(stmt),
None => break,
}
}
let else_condition = if self.current_token() == Token::Else {
self.cursor += 1;
let mut else_condition = Vec::new();
while self.current_token() != Token::EndIf {
match self.next()? {
Some(stmt) => else_condition.push(stmt),
None => break,
}
}
Some(else_condition)
} else {
None
};
if self.current_token() == Token::EndIf {
self.cursor += 1;
} else {
return Err(TemplateParseError::new(format!(
"expected endif, found: {}",
self.current_token()
)));
}
Ok(Some(Stmt::If {
var,
condition,
else_condition,
}))
} else {
Ok(None)
}
}
fn next(&mut self) -> Result<Option<Stmt<'a>>> {
self.skip_brackets();
if self.cursor >= self.len {
return Ok(None);
}
if let t @ Token::Invalid(_) = self.current_token() {
return Err(TemplateParseError {
message: t.to_string(),
});
}
let text = self.consume_text();
if text.is_some() {
return Ok(text.map(Stmt::Text));
}
let var = self.consume_var();
if var.is_some() {
return Ok(var.map(Stmt::Var));
}
let if_ = self.consume_if()?;
if if_.is_some() {
return Ok(if_);
}
Ok(None)
}
}
pub fn render<T: ToString>(template: &str, data: &HashMap<&str, T>) -> Result<String> {
let tokens: Vec<Token> = Lexer::new(template).collect();
let mut parser = Parser::new(&tokens);
let mut stmts: Vec<Stmt> = Vec::new();
while let Some(stmt) = parser.next()? {
stmts.push(stmt);
}
let mut out = String::new();
for stmt in stmts {
stmt.execute(&mut out, data)?;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn it_replaces_variable() {
let template = "<html>Hello {% name %}</html>";
let data: HashMap<&str, &str> = [("name", "world")].into();
let rendered = render(template, &data).expect("it should render");
assert_eq!(rendered, "<html>Hello world</html>")
}
#[test]
fn it_replaces_variable_with_new_lines() {
let template = r#"
<html>
<h1>Hello<h2>
<em>{% name %}</em>
</html>"#;
let data: HashMap<&str, &str> = [("name", "world")].into();
let rendered = render(template, &data).expect("it should render");
let expected = r#"
<html>
<h1>Hello<h2>
<em>world</em>
</html>"#;
assert_eq!(rendered, expected)
}
#[test]
fn it_performs_condition() {
let template = "<html>Hello {% if alpha %}alpha{% else %}stable{% endif %}</html>";
let data: HashMap<&str, bool> = [("alpha", true)].into();
let rendered = render(template, &data).expect("it should render");
assert_eq!(rendered, "<html>Hello alpha</html>")
}
#[test]
fn it_performs_else_condition() {
let template = "<html>Hello {% if alpha %}alpha{% else %}stable{% endif %}</html>";
let data: HashMap<&str, bool> = [("alpha", false)].into();
let rendered = render(template, &data).expect("it should render");
assert_eq!(rendered, "<html>Hello stable</html>")
}
#[test]
fn it_performs_condition_with_new_lines() {
let template = r#"
<html>
<h1>Hello<h2>{% if alpha %}
<em>alpha</em>{% else %}
<em>stable</em>{% endif %}
</html>"#;
let data: HashMap<&str, bool> = [("alpha", true)].into();
let rendered = render(template, &data).expect("it should render");
let expected = r#"
<html>
<h1>Hello<h2>
<em>alpha</em>
</html>"#;
assert_eq!(rendered, expected)
}
#[test]
fn it_replaces_variable_within_if() {
let template = r#"
<html>
<h1>Hello<h2>{% if alpha %}
<em>{% alpha_str %}</em>{% else %}
<em>stable</em>{% endif %}
</html>"#;
let data: HashMap<&str, &str> = [("alpha", "true"), ("alpha_str", "holla alpha")].into();
let rendered = render(template, &data).expect("it should render");
let expected = r#"
<html>
<h1>Hello<h2>
<em>holla alpha</em>
</html>"#;
assert_eq!(rendered, expected)
}
#[test]
fn it_performs_nested_conditions() {
let template = r#"
<html>
<h1>Hello<h2>{% if alpha %}
<em>{% alpha_str %}</em>{% else %}
<em>{% if beta %}beta{%else%}stable{%endif%}</em>{% endif %}
</html>"#;
let data: HashMap<&str, &str> = [
("alpha", "false"),
("beta", "true"),
("alpha_str", "holla alpha"),
]
.into();
let rendered = render(template, &data).expect("it should render");
let expected = r#"
<html>
<h1>Hello<h2>
<em>beta</em>
</html>"#;
assert_eq!(rendered, expected)
}
#[test]
fn it_panics() {
let template = "<html>Hello {% name }</html>";
let data: HashMap<&str, &str> = [("name", "world")].into();
let rendered = render(template, &data);
assert!(rendered.is_err())
}
}

33
packages/cli/src/main.rs Normal file
View File

@ -0,0 +1,33 @@
use std::{env::args_os, ffi::OsStr, path::Path};
fn main() {
let mut args = args_os().peekable();
let mut is_cargo = false;
let bin_name = match args
.next()
.as_deref()
.map(Path::new)
.and_then(Path::file_stem)
.and_then(OsStr::to_str)
{
Some("cargo-create-makepad-app") => {
is_cargo = true;
if args.peek().and_then(|s| s.to_str()) == Some("create-makepad-app") {
// remove the extra cargo subcommand
args.next();
Some("cargo create-makepad-app".into())
} else {
Some("cargo-create-makepad-app".into())
}
}
Some(stem) => Some(stem.to_string()),
None => None,
};
create_makepad_app::run(
args,
bin_name,
if is_cargo { Some("cargo".into()) } else { None },
);
}

View File

@ -0,0 +1,197 @@
use std::collections::HashMap;
use anyhow::{bail, Context};
#[derive(Default, Clone, PartialEq, Eq, Debug)]
pub struct Manifest<'a> {
pub before_dev_command: Option<&'a str>,
pub before_build_command: Option<&'a str>,
pub dev_path: Option<&'a str>,
pub dist_dir: Option<&'a str>,
pub with_global_makepad: Option<bool>,
pub files: HashMap<&'a str, &'a str>,
}
impl<'a> Manifest<'a> {
pub fn parse(s: &'a str, mobile: bool) -> Result<Self, anyhow::Error> {
let mut manifest = Self::default();
let mut in_files_section = false;
let mut in_mobile_section = false;
for (i, line) in s.split('\n').enumerate() {
let line_number = i + 1;
// ignore the comment portion of the line
let line = line.split('#').next().unwrap().trim();
if line.is_empty() {
continue;
}
if line == "[files]" {
in_files_section = true;
in_mobile_section = false;
continue;
}
if line == "[mobile]" {
in_mobile_section = true;
in_files_section = false;
continue;
}
if line.contains('=') {
let mut s = line.split('=');
let (k, v) = (
s.next()
.with_context(|| {
format!("parsing manifest: key is not found in line {line_number}")
})?
.trim(),
s.next()
.with_context(|| {
format!("parsing manifest: value is not found in line {line_number}")
})?
.trim(),
);
if k.is_empty() {
bail!("parsing manifest: key is empty in line {line_number}");
}
if v.is_empty() {
bail!("parsing manifest: value is empty in line {line_number}");
}
#[allow(clippy::nonminimal_bool)]
let replace =
!in_files_section && (!in_mobile_section || (in_mobile_section && mobile));
match k {
"beforeDevCommand" if replace => manifest.before_dev_command = Some(v),
"beforeBuildCommand" if replace => manifest.before_build_command = Some(v),
"devPath" if replace => manifest.dev_path = Some(v),
"distDir" if replace => manifest.dist_dir = Some(v),
"withGlobalTauri" if replace => manifest.with_global_makepad = Some(v.parse()?),
_ if in_files_section => {
manifest.files.insert(k, v);
}
_ => {}
}
}
}
Ok(manifest)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn it_parses() {
let manifest_file = r#"
# Copyright 2019-2022 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
beforeDevCommand = npm start -- --port 1420
beforeBuildCommand = {% pkg_manager_run_command %} build # this comment should be stripped
devPath = http://localhost:1420
[mobile]
beforeBuildCommand = {% pkg_manager_run_command %} build mobile
[files]
makepad.svg = src/assets/makepad.svg
styles.css = src/styles.css
"#;
assert_eq!(Manifest::parse(manifest_file, false).unwrap(), {
let mut files = HashMap::new();
files.insert("makepad.svg", "src/assets/makepad.svg");
files.insert("styles.css", "src/styles.css");
Manifest {
before_dev_command: Some("npm start -- --port 1420"),
before_build_command: Some("{% pkg_manager_run_command %} build"),
dev_path: Some("http://localhost:1420"),
dist_dir: None,
with_global_makepad: None,
files,
}
});
assert_eq!(Manifest::parse(manifest_file, true).unwrap(), {
let mut files = HashMap::new();
files.insert("makepad.svg", "src/assets/makepad.svg");
files.insert("styles.css", "src/styles.css");
Manifest {
before_dev_command: Some("npm start -- --port 1420"),
before_build_command: Some("{% pkg_manager_run_command %} build mobile"),
dev_path: Some("http://localhost:1420"),
dist_dir: None,
with_global_makepad: None,
files,
}
});
}
#[test]
#[should_panic]
fn it_panics_while_parsing() {
let manifest_file = r#"
# Copyright 2019-2022 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
beforeDevCommand = npm start -- --port 1420
beforeBuildCommand =
devPath = http://localhost:1420
[mobile]
beforeBuildCommand = {% pkg_manager_run_command %} build mobile
[files]
makepad.svg = src/assets/makepad.svg
styles.css = src/styles.css
"#;
Manifest::parse(manifest_file, false).unwrap();
}
#[test]
fn later_should_override_former() {
let manifest_file = r#"
# Copyright 2019-2022 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
beforeDevCommand = npm start -- --port 1420
beforeBuildCommand = {% pkg_manager_run_command %} build # this comment should be stripped
devPath = http://localhost:1420
beforeBuildCommand = {% pkg_manager_run_command %} build mobile
[files]
makepad.svg = src/assets/makepad.svg
styles.css = src/styles.css
"#;
assert_eq!(Manifest::parse(manifest_file, false).unwrap(), {
let mut files = HashMap::new();
files.insert("makepad.svg", "src/assets/makepad.svg");
files.insert("styles.css", "src/styles.css");
Manifest {
before_dev_command: Some("npm start -- --port 1420"),
before_build_command: Some("{% pkg_manager_run_command %} build mobile"),
dev_path: Some("http://localhost:1420"),
dist_dir: None,
with_global_makepad: None,
files,
}
});
}
}

View File

@ -0,0 +1,84 @@
use std::{fmt::Display, str::FromStr};
use crate::{colors::*, template::Template};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PackageManager {
Cargo,
Unknown,
}
impl Default for PackageManager {
fn default() -> Self {
PackageManager::Cargo
}
}
impl<'a> PackageManager {
pub const ALL: &'a [PackageManager] = &[
PackageManager::Cargo
];
}
impl PackageManager {
/// Returns templates without flavors
pub const fn templates_no_flavors(&self) -> &[Template] {
match self {
PackageManager::Cargo => &[
Template::MakepadCounter,
Template::MakepadStackNavigation,
Template::MakepadLogin,
],
PackageManager::Unknown => &[],
}
}
pub const fn templates(&self) -> &[Template] {
match self {
PackageManager::Cargo => &[
Template::MakepadCounter,
Template::MakepadStackNavigation,
Template::MakepadLogin,
],
PackageManager::Unknown => &[],
}
}
pub const fn install_cmd(&self) -> Option<&str> {
match self {
_ => None,
}
}
pub const fn run_cmd(&self) -> &str {
match self {
PackageManager::Cargo => "cargo",
PackageManager::Unknown => "unknown",
}
}
}
impl Display for PackageManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PackageManager::Cargo => write!(f, "cargo"),
PackageManager::Unknown => write!(f, "unknown"),
}
}
}
impl FromStr for PackageManager {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cargo" => Ok(PackageManager::Cargo),
_ => Err(format!(
"{YELLOW}{s}{RESET} is not a valid package manager. Valid package mangers are [{}]",
PackageManager::ALL
.iter()
.map(|e| format!("{GREEN}{e}{RESET}"))
.collect::<Vec<_>>()
.join(", ")
)),
}
}
}

View File

@ -0,0 +1,334 @@
use std::{collections::HashMap, fmt::Display, fs, io::Write, path, str::FromStr};
use anyhow::Context;
use rust_embed::RustEmbed;
use crate::{colors::*, manifest::Manifest, package_manager::PackageManager};
#[derive(RustEmbed)]
#[folder = "fragments"]
#[allow(clippy::upper_case_acronyms)]
struct FRAGMENTS;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Template {
MakepadCounter,
MakepadStackNavigation,
MakepadLogin,
Unknown,
}
impl Default for Template {
fn default() -> Self {
Template::MakepadCounter
}
}
impl Template {
pub const fn select_text<'a>(&self) -> &'a str {
match self {
Template::MakepadCounter => "Counter",
Template::MakepadStackNavigation => "SimpleStackNavigation",
Template::MakepadLogin => "SimpleLogin",
Template::Unknown => "Unknown",
_ => unreachable!(),
}
}
}
impl Display for Template {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Template::MakepadCounter => write!(f, "counter"),
Template::MakepadStackNavigation => write!(f, "simplestacknavigation"),
Template::MakepadLogin => write!(f, "simplelogin"),
Template::Unknown => write!(f, "unknown"),
}
}
}
impl FromStr for Template {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"counter" => Ok(Template::MakepadCounter),
"simplestacknavigation" => Ok(Template::MakepadStackNavigation),
"simplelogin" => Ok(Template::MakepadLogin),
_ => Err(format!(
"{YELLOW}{s}{RESET} is not a valid template. Valid templates are [{}]",
Template::ALL
.iter()
.map(|e| format!("{GREEN}{e}{RESET}"))
.collect::<Vec<_>>()
.join(", ")
)),
}
}
}
impl<'a> Template {
pub const ALL: &'a [Template] = &[
Template::MakepadCounter,
Template::MakepadStackNavigation,
Template::MakepadLogin,
];
pub fn flavors<'b>(&self, pkg_manager: PackageManager) -> Option<&'b [Flavor]> {
match self {
Template::MakepadCounter => {
if pkg_manager == PackageManager::Cargo {
None
} else {
Some(&[Flavor::Unknown])
}
}
Template::MakepadStackNavigation => {
if pkg_manager == PackageManager::Cargo {
None
} else {
Some(&[Flavor::Unknown])
}
}
_ => None,
}
}
pub fn from_flavor(&self, flavor: Flavor) -> Self {
match (self, flavor) {
(Template::Unknown, Flavor::Unknown) => Template::Unknown,
_ => *self,
}
}
pub fn without_flavor(&self) -> Self {
match self {
Template::Unknown => Template::Unknown,
_ => *self,
}
}
pub const fn possible_package_managers(&self) -> &[PackageManager] {
match self {
Template::MakepadCounter => &[PackageManager::Cargo,],
Template::MakepadStackNavigation => &[PackageManager::Cargo],
Template::MakepadLogin => &[PackageManager::Cargo],
Template::Unknown => &[],
}
}
pub const fn needs_trunk(&self) -> bool {
matches!(self, Template::MakepadCounter | Template::MakepadStackNavigation | Template::MakepadLogin)
}
pub const fn needs_makepad_cli(&self) -> bool {
matches!(
self,
Template::MakepadCounter | Template::MakepadStackNavigation | Template::MakepadLogin
)
}
pub const fn needs_wasm32_target(&self) -> bool {
matches!(self, Template::MakepadCounter | Template::MakepadStackNavigation | Template::MakepadLogin)
}
pub fn render(
&self,
target_dir: &path::Path,
pkg_manager: PackageManager,
project_name: &str,
package_name: &str,
alpha: bool,
mobile: bool,
) -> anyhow::Result<()> {
let manifest_bytes = FRAGMENTS::get(&format!("fragment-{self}/_cta_manifest_"))
.with_context(|| "Failed to get manifest bytes")?
.data;
let manifest_str = String::from_utf8(manifest_bytes.to_vec())?;
let manifest = Manifest::parse(&manifest_str, mobile)?;
let lib_name = format!("{}_lib", package_name.replace('-', "_"));
let manifest_template_data: HashMap<&str, &str> = [
("pkg_manager_run_command", pkg_manager.run_cmd()),
("lib_name", &lib_name),
("project_name", project_name),
("package_name", package_name),
(
"double_dash_with_space",
if pkg_manager == PackageManager::Unknown {
"-- "
} else {
""
},
),
]
.into();
let template_data: HashMap<&str, String> = [
("stable", (!alpha).to_string()),
("alpha", alpha.to_string()),
("mobile", mobile.to_string()),
("project_name", project_name.to_string()),
("package_name", package_name.to_string()),
(
"before_dev_command",
crate::lte::render(
manifest.before_dev_command.unwrap_or_default(),
&manifest_template_data,
)?,
),
(
"before_build_command",
crate::lte::render(
manifest.before_build_command.unwrap_or_default(),
&manifest_template_data,
)?,
),
(
"dev_path",
crate::lte::render(
manifest.dev_path.unwrap_or_default(),
&manifest_template_data,
)?,
),
(
"dist_dir",
crate::lte::render(
manifest.dist_dir.unwrap_or_default(),
&manifest_template_data,
)?,
),
(
"with_global_makepad",
manifest.with_global_makepad.unwrap_or_default().to_string(),
),
("lib_name", lib_name),
]
.into();
let write_file = |file: &str, template_data| -> anyhow::Result<()> {
// remove the first component, which is certainly the fragment directory they were in before getting embeded into the binary
let p = path::PathBuf::from(file)
.components()
.skip(1)
.collect::<Vec<_>>()
.iter()
.collect::<path::PathBuf>();
let p = target_dir.join(p);
let file_name = p.file_name().unwrap().to_string_lossy();
let file_name = match &*file_name {
"_gitignore" => ".gitignore",
// skip manifest
"_cta_manifest_" => return Ok(()),
// conditional files:
// are files that start with a special syntax
// "%(<list of flags separated by `-`>%)<file_name>"
// flags are supported package managers, stable, alpha and mobile.
// example: "%(pnpm-npm-yarn-stable-alpha)%package.json"
name if name.starts_with("%(") && name[1..].contains(")%") => {
let mut s = name.strip_prefix("%(").unwrap().split(")%");
let (mut flags, name) = (
s.next().unwrap().split('-').collect::<Vec<_>>(),
s.next().unwrap(),
);
let for_stable = flags.contains(&"stable");
let for_alpha = flags.contains(&"alpha");
let for_mobile = flags.contains(&"mobile");
// remove these flags to only keep package managers flags
flags.retain(|e| !["stable", "alpha", "mobile"].contains(e));
if ((for_stable && !alpha)
|| (for_alpha && alpha && !mobile)
|| (for_mobile && alpha && mobile)
|| (!for_stable && !for_alpha && !for_mobile))
&& (flags.contains(&pkg_manager.to_string().as_str()) || flags.is_empty())
{
name
} else {
// skip writing this file
return Ok(());
}
}
name => name,
};
// Only modify files that need to use the template engine
let (file_data, file_name) = if let Some(file_name) = file_name.strip_suffix(".lts") {
let file_data = FRAGMENTS::get(file).unwrap().data.to_vec();
let file_data_as_str = std::str::from_utf8(&file_data)?;
(
crate::lte::render(file_data_as_str, template_data)?.into_bytes(),
file_name,
)
} else {
(FRAGMENTS::get(file).unwrap().data.to_vec(), file_name)
};
let parent = p.parent().unwrap();
fs::create_dir_all(parent)?;
fs::write(parent.join(file_name), file_data)?;
Ok(())
};
// 1. write base files
for file in FRAGMENTS::iter().filter(|e| {
path::PathBuf::from(e.to_string())
.components()
.next()
.unwrap()
.as_os_str()
== "_base_"
}) {
write_file(&file, &template_data)?;
}
// 2. write template files which can override files from base
for file in FRAGMENTS::iter().filter(|e| {
path::PathBuf::from(e.to_string())
.components()
.next()
.unwrap()
.as_os_str()
== path::PathBuf::from(format!("fragment-{self}"))
}) {
write_file(&file, &template_data)?;
}
// 3. write extra files specified in the fragment manifest
for (src, dest) in manifest.files {
let data = FRAGMENTS::get(&format!("_assets_/{src}"))
.with_context(|| format!("Failed to get asset file bytes: {src}"))?
.data;
let dest = target_dir.join(dest);
let parent = dest.parent().unwrap();
fs::create_dir_all(parent)?;
let mut file = fs::OpenOptions::new()
.append(true)
.create(true)
.open(dest)?;
file.write_all(&data)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Flavor {
Unknown,
}
impl Display for Flavor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Flavor::Unknown => write!(f, "Unknown"),
}
}
}

449
packages/cli/src/theme.rs Normal file
View File

@ -0,0 +1,449 @@
use std::fmt;
use dialoguer::{
console::{style, Style, StyledObject},
theme::Theme,
};
pub struct ColorfulTheme {
/// The style for default values
pub defaults_style: Style,
/// The style for prompt
pub prompt_style: Style,
/// Prompt prefix value and style
pub prompt_prefix: StyledObject<String>,
/// Prompt suffix value and style
pub prompt_suffix: StyledObject<String>,
/// Prompt on success prefix value and style
pub success_prefix: StyledObject<String>,
/// Prompt on success suffix value and style
pub success_suffix: StyledObject<String>,
/// Error prefix value and style
pub error_prefix: StyledObject<String>,
/// The style for error message
pub error_style: Style,
/// The style for hints
pub hint_style: Style,
/// The style for values on prompt success
pub values_style: Style,
/// The style for active items
pub active_item_style: Style,
/// The style for inactive items
pub inactive_item_style: Style,
/// Active item in select prefix value and style
pub active_item_prefix: StyledObject<String>,
/// Inctive item in select prefix value and style
pub inactive_item_prefix: StyledObject<String>,
/// Checked item in multi select prefix value and style
pub checked_item_prefix: StyledObject<String>,
/// Unchecked item in multi select prefix value and style
pub unchecked_item_prefix: StyledObject<String>,
/// Picked item in sort prefix value and style
pub picked_item_prefix: StyledObject<String>,
/// Unpicked item in sort prefix value and style
pub unpicked_item_prefix: StyledObject<String>,
/// Formats the cursor for a fuzzy select prompt
#[cfg(feature = "fuzzy-select")]
pub fuzzy_cursor_style: Style,
// Formats the highlighting if matched characters
#[cfg(feature = "fuzzy-select")]
pub fuzzy_match_highlight_style: Style,
/// Show the selections from certain prompts inline
pub inline_selections: bool,
}
impl Default for ColorfulTheme {
fn default() -> ColorfulTheme {
ColorfulTheme {
defaults_style: Style::new().for_stderr().cyan(),
prompt_style: Style::new().for_stderr().bold(),
prompt_prefix: style("?".to_string()).for_stderr().yellow(),
prompt_suffix: style("".to_string()).for_stderr().black().bright(),
success_prefix: style("".to_string()).for_stderr().green(),
success_suffix: style("·".to_string()).for_stderr().black().bright(),
error_prefix: style("".to_string()).for_stderr().red(),
error_style: Style::new().for_stderr().red(),
hint_style: Style::new().for_stderr().black().bright(),
values_style: Style::new().for_stderr().green(),
active_item_style: Style::new().for_stderr().cyan(),
inactive_item_style: Style::new().for_stderr(),
active_item_prefix: style("".to_string()).for_stderr().green(),
inactive_item_prefix: style(" ".to_string()).for_stderr(),
checked_item_prefix: style("".to_string()).for_stderr().green(),
unchecked_item_prefix: style("".to_string()).for_stderr().black(),
picked_item_prefix: style("".to_string()).for_stderr().green(),
unpicked_item_prefix: style(" ".to_string()).for_stderr(),
#[cfg(feature = "fuzzy-select")]
fuzzy_cursor_style: Style::new().for_stderr().black().on_white(),
#[cfg(feature = "fuzzy-select")]
fuzzy_match_highlight_style: Style::new().for_stderr().bold(),
inline_selections: true,
}
}
}
impl Theme for ColorfulTheme {
/// Formats a prompt.
fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
if !prompt.is_empty() {
write!(
f,
"{} {} ",
&self.prompt_prefix,
self.prompt_style.apply_to(prompt)
)?;
}
write!(f, "{}", &self.prompt_suffix)
}
/// Formats an error
fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result {
write!(
f,
"{} {}",
&self.error_prefix,
self.error_style.apply_to(err)
)
}
/// Formats an input prompt.
fn format_input_prompt(
&self,
f: &mut dyn fmt::Write,
prompt: &str,
default: Option<&str>,
) -> fmt::Result {
if !prompt.is_empty() {
write!(
f,
"{} {} ",
&self.prompt_prefix,
self.prompt_style.apply_to(prompt)
)?;
}
match default {
Some(default) => write!(
f,
"{} {} ",
self.hint_style.apply_to(&format!("({default})")),
&self.prompt_suffix
),
None => write!(f, "{} ", &self.prompt_suffix),
}
}
/// Formats a confirm prompt.
fn format_confirm_prompt(
&self,
f: &mut dyn fmt::Write,
prompt: &str,
default: Option<bool>,
) -> fmt::Result {
if !prompt.is_empty() {
write!(
f,
"{} {} ",
&self.prompt_prefix,
self.prompt_style.apply_to(prompt)
)?;
}
match default {
None => write!(
f,
"{} {}",
self.hint_style.apply_to("(y/n)"),
&self.prompt_suffix
),
Some(true) => write!(
f,
"{} {} {}",
self.hint_style.apply_to("(y/n)"),
&self.prompt_suffix,
self.defaults_style.apply_to("yes")
),
Some(false) => write!(
f,
"{} {} {}",
self.hint_style.apply_to("(y/n)"),
&self.prompt_suffix,
self.defaults_style.apply_to("no")
),
}
}
/// Formats a confirm prompt after selection.
fn format_confirm_prompt_selection(
&self,
f: &mut dyn fmt::Write,
prompt: &str,
selection: Option<bool>,
) -> fmt::Result {
if !prompt.is_empty() {
write!(
f,
"{} {} ",
&self.success_prefix,
self.prompt_style.apply_to(prompt)
)?;
}
let selection = selection.map(|b| if b { "yes" } else { "no" });
match selection {
Some(selection) => {
write!(
f,
"{} {}",
&self.success_suffix,
self.values_style.apply_to(selection)
)
}
None => {
write!(f, "{}", &self.success_suffix)
}
}
}
/// Formats an input prompt after selection.
fn format_input_prompt_selection(
&self,
f: &mut dyn fmt::Write,
prompt: &str,
sel: &str,
) -> fmt::Result {
if !prompt.is_empty() {
write!(
f,
"{} {} ",
&self.success_prefix,
self.prompt_style.apply_to(prompt)
)?;
}
write!(
f,
"{} {}",
&self.success_suffix,
self.values_style.apply_to(sel)
)
}
/// Formats a password prompt after selection.
#[cfg(feature = "password")]
fn format_password_prompt_selection(
&self,
f: &mut dyn fmt::Write,
prompt: &str,
) -> fmt::Result {
self.format_input_prompt_selection(f, prompt, "********")
}
/// Formats a multi select prompt after selection.
fn format_multi_select_prompt_selection(
&self,
f: &mut dyn fmt::Write,
prompt: &str,
selections: &[&str],
) -> fmt::Result {
if !prompt.is_empty() {
write!(
f,
"{} {} ",
&self.success_prefix,
self.prompt_style.apply_to(prompt)
)?;
}
write!(f, "{} ", &self.success_suffix)?;
if self.inline_selections {
for (idx, sel) in selections.iter().enumerate() {
write!(
f,
"{}{}",
if idx == 0 { "" } else { ", " },
self.values_style.apply_to(sel)
)?;
}
}
Ok(())
}
/// Formats a select prompt item.
fn format_select_prompt_item(
&self,
f: &mut dyn fmt::Write,
text: &str,
active: bool,
) -> fmt::Result {
let (text, desc) = text.split_once('-').unwrap_or((text, ""));
if active {
write!(
f,
"{} {}{}",
self.active_item_prefix,
self.active_item_style.apply_to(text),
self.hint_style.apply_to(desc),
)
} else {
write!(
f,
"{} {}",
self.inactive_item_prefix,
self.inactive_item_style.apply_to(text),
)
}
}
/// Formats a multi select prompt item.
fn format_multi_select_prompt_item(
&self,
f: &mut dyn fmt::Write,
text: &str,
checked: bool,
active: bool,
) -> fmt::Result {
let details = match (checked, active) {
(true, true) => (
&self.checked_item_prefix,
self.active_item_style.apply_to(text),
),
(true, false) => (
&self.checked_item_prefix,
self.inactive_item_style.apply_to(text),
),
(false, true) => (
&self.unchecked_item_prefix,
self.active_item_style.apply_to(text),
),
(false, false) => (
&self.unchecked_item_prefix,
self.inactive_item_style.apply_to(text),
),
};
write!(f, "{} {}", details.0, details.1)
}
/// Formats a sort prompt item.
fn format_sort_prompt_item(
&self,
f: &mut dyn fmt::Write,
text: &str,
picked: bool,
active: bool,
) -> fmt::Result {
let details = match (picked, active) {
(true, true) => (
&self.picked_item_prefix,
self.active_item_style.apply_to(text),
),
(false, true) => (
&self.unpicked_item_prefix,
self.active_item_style.apply_to(text),
),
(_, false) => (
&self.unpicked_item_prefix,
self.inactive_item_style.apply_to(text),
),
};
write!(f, "{} {}", details.0, details.1)
}
/// Formats a fuzzy select prompt item.
#[cfg(feature = "fuzzy-select")]
fn format_fuzzy_select_prompt_item(
&self,
f: &mut dyn fmt::Write,
text: &str,
active: bool,
highlight_matches: bool,
matcher: &SkimMatcherV2,
search_term: &str,
) -> fmt::Result {
write!(
f,
"{} ",
if active {
&self.active_item_prefix
} else {
&self.inactive_item_prefix
}
)?;
if highlight_matches {
if let Some((_score, indices)) = matcher.fuzzy_indices(text, &search_term) {
for (idx, c) in text.chars().into_iter().enumerate() {
if indices.contains(&idx) {
if active {
write!(
f,
"{}",
self.active_item_style
.apply_to(self.fuzzy_match_highlight_style.apply_to(c))
)?;
} else {
write!(f, "{}", self.fuzzy_match_highlight_style.apply_to(c))?;
}
} else {
if active {
write!(f, "{}", self.active_item_style.apply_to(c))?;
} else {
write!(f, "{}", c)?;
}
}
}
return Ok(());
}
}
write!(f, "{}", text)
}
/// Formats a fuzzy-selectprompt after selection.
#[cfg(feature = "fuzzy-select")]
fn format_fuzzy_select_prompt(
&self,
f: &mut dyn fmt::Write,
prompt: &str,
search_term: &str,
cursor_pos: usize,
) -> fmt::Result {
if !prompt.is_empty() {
write!(
f,
"{} {} ",
&self.prompt_prefix,
self.prompt_style.apply_to(prompt)
)?;
}
if cursor_pos < search_term.len() {
let st_head = search_term[0..cursor_pos].to_string();
let st_tail = search_term[cursor_pos + 1..search_term.len()].to_string();
let st_cursor = self
.fuzzy_cursor_style
.apply_to(search_term.to_string().chars().nth(cursor_pos).unwrap());
write!(
f,
"{} {}{}{}",
&self.prompt_suffix, st_head, st_cursor, st_tail
)
} else {
let cursor = self.fuzzy_cursor_style.apply_to(" ");
write!(
f,
"{} {}{}",
&self.prompt_suffix,
search_term.to_string(),
cursor
)
}
}
}