feat(tauri) add app CLI interface config (#670)

This commit is contained in:
Lucas Fernandes Nogueira 2020-06-14 23:36:35 -03:00 committed by GitHub
parent 577a044bfa
commit 14a1ddfe18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 867 additions and 299 deletions

View File

@ -0,0 +1,7 @@
---
"tauri.js": minor
"tauri": minor
"tauri-api": minor
---
Adds a command line interface option to tauri apps, configurable under tauri.conf.json > tauri > cli.

View File

@ -21,8 +21,8 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
env:
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/src-tauri
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
eslint-check:
runs-on: ubuntu-latest

View File

@ -58,8 +58,8 @@ jobs:
if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION
working-directory: ${{ matrix.package.path }}
env:
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/src-tauri
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
run: |
cargo package --no-verify
echo "We will publish:" $PACKAGE_VERSION
@ -68,8 +68,8 @@ jobs:
if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION
working-directory: ${{ matrix.package.path }}
env:
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/src-tauri
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
run: |
cargo install cargo-audit
echo "# Cargo Audit" | tee -a ${{runner.workspace }}/notes.md
@ -80,8 +80,8 @@ jobs:
if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION
working-directory: ${{ matrix.package.path }}
env:
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/src-tauri
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
run: |
echo "# Cargo Publish" | tee -a ${{runner.workspace }}/notes.md
echo "\`\`\`" >> ${{runner.workspace }}/notes.md

View File

@ -27,14 +27,14 @@ jobs:
cd ./tauri
cargo build
env:
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/src-tauri
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
- name: test
run: |
cargo test
env:
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/test/fixture/src-tauri
TAURI_DIST_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/dist
TAURI_DIR: ${{ runner.workspace }}/tauri/tauri/examples/communication/src-tauri
build-tauri-bundler:
runs-on: ${{ matrix.platform }}

12
cli/tauri.js/api/cli.js Normal file
View File

@ -0,0 +1,12 @@
import tauri from './tauri'
/**
* gets the CLI matches
*/
function getMatches() {
return tauri.cliMatches()
}
export {
getMatches
}

View File

@ -500,6 +500,10 @@ class Runner {
tomlFeatures.push('edge')
}
if (cfg.tauri.cli) {
tomlFeatures.push('cli')
}
if (typeof manifest.dependencies.tauri === 'string') {
manifest.dependencies.tauri = {
version: manifest.dependencies.tauri,

View File

@ -7,6 +7,7 @@ export default {
},
ctx: {},
tauri: {
cli: null,
embeddedServer: {
active: true
},

View File

@ -1,6 +1,39 @@
// TODO: Clean up types, properly mark which ones are optional
// May need to have different types for each stage of config generation process
export interface CliArg {
short?: string
name: string
description?: string
longDescription?: string
takesValue?: boolean
multiple?: boolean
possibleValues?: string[]
minValues?: number
maxValues?: number
required?: boolean
requiredUnless?: string
requiredUnlessAll?: string[]
requiredUnlessOne?: string[]
conflictsWith?: string
conflictsWithAll?: string
requires?: string
requiresAll?: string[]
requiresIf?: [string, string]
requiredIf?: [string, string]
requireEquals?: boolean
global?: boolean
}
export interface CliConfig {
args?: CliArg[]
description?: string
longDescription?: string
beforeHelp?: string
afterHelp?: string
subcommands?: { [name: string]: CliConfig }
}
export interface TauriConfig {
build: {
distDir: string
@ -17,6 +50,7 @@ export interface TauriConfig {
exitOnPanic?: boolean
}
tauri: {
cli: CliConfig
inlinedAssets: string[]
devPath: string
embeddedServer: {

View File

@ -655,6 +655,21 @@ window.tauri = {
asset: assetName,
assetType: assetType || 'unknown'
})
},
cliMatches: function () {
<% if (tauri.cli) { %>
return this.promisified({
cmd: 'cliMatches'
})
<% } else { %>
<% if (ctx.dev) { %>
console.error('You must add the CLI args configuration under tauri.conf.json > tauri > cli')
return __reject()
<% } %>
return __reject()
<% } %>
}
};

View File

@ -11,7 +11,8 @@
"baseUrl": ".",
"paths": {
"types": ["src/types"]
}
},
"resolveJsonModule": true
},
"include": ["src"]
}

View File

@ -29,8 +29,13 @@ nfd = "0.0.4"
attohttpc = {version = "0.14.0", features=["json", "form" ]}
http = "0.2"
tauri-utils = {version = "0.5", path = "../tauri-utils"}
envmnt = "0.8.2"
clap = { git = "https://github.com/clap-rs/clap", rev = "1a276f8", version = "3.0.0-beta.1", optional = true }
[dev-dependencies]
quickcheck = "0.9.2"
quickcheck_macros = "0.9.1"
totems = "0.2.7"
[features]
cli = ["clap"]

153
tauri-api/src/cli.rs Normal file
View File

@ -0,0 +1,153 @@
use crate::config::{Cli, Config};
use clap::{App, Arg, ArgMatches};
use serde::Serialize;
use serde_json::Value;
use std::collections::HashMap;
#[macro_use]
mod macros;
#[derive(Default, Debug, Serialize)]
pub struct ArgData {
value: Value,
occurrences: u64,
}
#[derive(Default, Debug, Serialize)]
pub struct SubcommandMatches {
name: String,
matches: Matches,
}
#[derive(Default, Debug, Serialize)]
pub struct Matches {
args: HashMap<String, ArgData>,
subcommand: Option<Box<SubcommandMatches>>,
}
impl Matches {
pub(crate) fn set_arg(&mut self, name: String, value: ArgData) {
self.args.insert(name, value);
}
pub(crate) fn set_subcommand(&mut self, name: String, matches: Matches) {
self.subcommand = Some(Box::new(SubcommandMatches { name, matches }));
}
}
pub fn get_matches(config: Config) -> Matches {
let cli = config.tauri.cli.unwrap();
let about = cli
.description()
.unwrap_or(&crate_description!().to_string())
.to_string();
let app = get_app(crate_name!(), Some(&about), &cli);
let matches = app.get_matches();
get_matches_internal(&cli, &matches)
}
fn get_matches_internal<T: Cli + 'static>(config: &T, matches: &ArgMatches) -> Matches {
let mut cli_matches = Matches::default();
map_matches(config, matches, &mut cli_matches);
let (subcommand_name, subcommand_matches_option) = matches.subcommand();
if let Some(subcommand_matches) = subcommand_matches_option {
let mut subcommand_cli_matches = Matches::default();
map_matches(
config.subcommands().unwrap().get(subcommand_name).unwrap(),
subcommand_matches,
&mut subcommand_cli_matches,
);
cli_matches.set_subcommand(subcommand_name.to_string(), subcommand_cli_matches);
}
cli_matches
}
fn map_matches<T: Cli + 'static>(config: &T, matches: &ArgMatches, cli_matches: &mut Matches) {
if let Some(args) = config.args() {
for arg in args {
let occurrences = matches.occurrences_of(arg.name.clone());
let value = if occurrences == 0 || !arg.takes_value.unwrap_or(false) {
Value::Null
} else if arg.multiple.unwrap_or(false) {
matches
.values_of(arg.name.clone())
.map(|v| {
let mut values = Vec::new();
for value in v {
values.push(Value::String(value.to_string()));
}
Value::Array(values)
})
.unwrap_or(Value::Null)
} else {
matches
.value_of(arg.name.clone())
.map(|v| Value::String(v.to_string()))
.unwrap_or(Value::Null)
};
cli_matches.set_arg(arg.name.clone(), ArgData { value, occurrences });
}
}
}
fn get_app<'a, T: Cli + 'static>(name: &str, about: Option<&'a String>, config: &'a T) -> App<'a> {
let mut app = App::new(name)
.author(crate_authors!())
.version(crate_version!());
if let Some(about) = about {
app = app.about(&**about);
}
if let Some(long_description) = config.long_description() {
app = app.long_about(&**long_description);
}
if let Some(args) = config.args() {
for arg in args {
let arg_name = arg.name.as_ref();
let mut clap_arg = Arg::new(arg_name).long(arg_name);
if let Some(short) = arg.short {
clap_arg = clap_arg.short(short);
}
clap_arg = bind_string_arg!(arg, clap_arg, description, about);
clap_arg = bind_string_arg!(arg, clap_arg, long_description, long_about);
clap_arg = bind_value_arg!(arg, clap_arg, takes_value);
clap_arg = bind_value_arg!(arg, clap_arg, multiple);
clap_arg = bind_value_arg!(arg, clap_arg, multiple_occurrences);
clap_arg = bind_value_arg!(arg, clap_arg, number_of_values);
clap_arg = bind_string_slice_arg!(arg, clap_arg, possible_values);
clap_arg = bind_value_arg!(arg, clap_arg, min_values);
clap_arg = bind_value_arg!(arg, clap_arg, max_values);
clap_arg = bind_string_arg!(arg, clap_arg, required_unless, required_unless);
clap_arg = bind_value_arg!(arg, clap_arg, required);
clap_arg = bind_string_arg!(arg, clap_arg, required_unless, required_unless);
clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_all);
clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_one);
clap_arg = bind_string_arg!(arg, clap_arg, conflicts_with, conflicts_with);
clap_arg = bind_string_slice_arg!(arg, clap_arg, conflicts_with_all);
clap_arg = bind_string_arg!(arg, clap_arg, requires, requires);
clap_arg = bind_string_slice_arg!(arg, clap_arg, requires_all);
clap_arg = bind_if_arg!(arg, clap_arg, requires_if);
clap_arg = bind_if_arg!(arg, clap_arg, required_if);
clap_arg = bind_value_arg!(arg, clap_arg, require_equals);
app = app.arg(clap_arg);
}
}
if let Some(subcommands) = config.subcommands() {
for (subcommand_name, subcommand) in subcommands {
let clap_subcommand = get_app(&subcommand_name, subcommand.description(), subcommand);
app = app.subcommand(clap_subcommand);
}
}
app
}

View File

@ -0,0 +1,45 @@
macro_rules! bind_string_arg {
($arg:expr, $clap_arg:expr, $arg_name:ident, $clap_field:ident) => {{
let arg = $arg;
let mut clap_arg = $clap_arg;
if let Some(value) = &arg.$arg_name {
clap_arg = clap_arg.$clap_field(value);
}
clap_arg
}}
}
macro_rules! bind_value_arg {
($arg:expr, $clap_arg:expr, $field:ident) => {{
let arg = $arg;
let mut clap_arg = $clap_arg;
if let Some(value) = arg.$field {
clap_arg = clap_arg.$field(value);
}
clap_arg
}}
}
macro_rules! bind_string_slice_arg {
($arg:expr, $clap_arg:expr, $field:ident) => {{
let arg = $arg;
let mut clap_arg = $clap_arg;
if let Some(value) = &arg.$field {
let v: Vec<&str> = value.iter().map(|x| &**x).collect();
clap_arg = clap_arg.$field(&v);
}
clap_arg
}}
}
macro_rules! bind_if_arg {
($arg:expr, $clap_arg:expr, $field:ident) => {{
let arg = $arg;
let mut clap_arg = $clap_arg;
if let Some(value) = &arg.$field {
let v: Vec<&str> = value.iter().map(|x| &**x).collect();
clap_arg = clap_arg.$field(&v[0], &v[1]);
}
clap_arg
}}
}

View File

@ -1,5 +1,6 @@
use serde::Deserialize;
use std::collections::HashMap;
use std::{fs, path};
#[derive(PartialEq, Deserialize, Clone, Debug)]
@ -67,6 +68,99 @@ fn default_embedded_server() -> EmbeddedServerConfig {
}
}
#[derive(PartialEq, Deserialize, Clone, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct CliArg {
pub short: Option<char>,
pub name: String,
pub description: Option<String>,
pub long_description: Option<String>,
pub takes_value: Option<bool>,
pub multiple: Option<bool>,
pub multiple_occurrences: Option<bool>,
pub number_of_values: Option<u64>,
pub possible_values: Option<Vec<String>>,
pub min_values: Option<u64>,
pub max_values: Option<u64>,
pub required: Option<bool>,
pub required_unless: Option<String>,
pub required_unless_all: Option<Vec<String>>,
pub required_unless_one: Option<Vec<String>>,
pub conflicts_with: Option<String>,
pub conflicts_with_all: Option<Vec<String>>,
pub requires: Option<String>,
pub requires_all: Option<Vec<String>>,
pub requires_if: Option<Vec<String>>,
pub required_if: Option<Vec<String>>,
pub require_equals: Option<bool>,
}
#[derive(PartialEq, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CliSubcommand {
description: Option<String>,
long_description: Option<String>,
before_help: Option<String>,
after_help: Option<String>,
args: Option<Vec<CliArg>>,
subcommands: Option<HashMap<String, CliSubcommand>>,
}
#[derive(PartialEq, Deserialize, Clone, Debug)]
#[serde(tag = "cli", rename_all = "camelCase")]
pub struct CliConfig {
description: Option<String>,
long_description: Option<String>,
before_help: Option<String>,
after_help: Option<String>,
args: Option<Vec<CliArg>>,
subcommands: Option<HashMap<String, CliSubcommand>>,
}
pub trait Cli {
fn args(&self) -> Option<&Vec<CliArg>>;
fn subcommands(&self) -> Option<&HashMap<String, CliSubcommand>>;
fn description(&self) -> Option<&String>;
fn long_description(&self) -> Option<&String>;
fn before_help(&self) -> Option<&String>;
fn after_help(&self) -> Option<&String>;
}
macro_rules! impl_cli {
( $($field_name:ident),+ $(,)?) => {
$(
impl Cli for $field_name {
fn args(&self) -> Option<&Vec<CliArg>> {
self.args.as_ref()
}
fn subcommands(&self) -> Option<&HashMap<String, CliSubcommand>> {
self.subcommands.as_ref()
}
fn description(&self) -> Option<&String> {
self.description.as_ref()
}
fn long_description(&self) -> Option<&String> {
self.description.as_ref()
}
fn before_help(&self) -> Option<&String> {
self.before_help.as_ref()
}
fn after_help(&self) -> Option<&String> {
self.after_help.as_ref()
}
}
)+
}
}
impl_cli!(CliSubcommand, CliConfig);
#[derive(PartialEq, Deserialize, Clone, Debug)]
#[serde(tag = "tauri", rename_all = "camelCase")]
pub struct TauriConfig {
@ -74,6 +168,8 @@ pub struct TauriConfig {
pub window: WindowConfig,
#[serde(default = "default_embedded_server")]
pub embedded_server: EmbeddedServerConfig,
#[serde(default)]
pub cli: Option<CliConfig>,
}
#[derive(PartialEq, Deserialize, Clone, Debug)]
@ -100,6 +196,7 @@ fn default_tauri() -> TauriConfig {
TauriConfig {
window: default_window(),
embedded_server: default_embedded_server(),
cli: None,
}
}
@ -127,22 +224,74 @@ mod test {
use super::*;
// generate a test_config based on the test fixture
fn create_test_config() -> Config {
let mut subcommands = std::collections::HashMap::new();
subcommands.insert(
"update".to_string(),
CliSubcommand {
description: Some("Updates the app".to_string()),
long_description: None,
before_help: None,
after_help: None,
args: Some(vec![CliArg {
short: Some('b'),
name: "background".to_string(),
description: Some("Update in background".to_string()),
..Default::default()
}]),
subcommands: None,
},
);
Config {
tauri: TauriConfig {
window: WindowConfig {
width: 800,
height: 600,
resizable: true,
title: String::from("Tauri App"),
title: String::from("Tauri API Validation"),
fullscreen: false,
},
embedded_server: EmbeddedServerConfig {
host: String::from("http://127.0.0.1"),
port: String::from("random"),
},
cli: Some(CliConfig {
description: Some("Tauri communication example".to_string()),
long_description: None,
before_help: None,
after_help: None,
args: Some(vec![
CliArg {
short: Some('c'),
name: "config".to_string(),
takes_value: Some(true),
description: Some("Config path".to_string()),
..Default::default()
},
CliArg {
short: Some('t'),
name: "theme".to_string(),
takes_value: Some(true),
description: Some("App theme".to_string()),
possible_values: Some(vec![
"light".to_string(),
"dark".to_string(),
"system".to_string(),
]),
..Default::default()
},
CliArg {
short: Some('v'),
name: "verbose".to_string(),
multiple_occurrences: Some(true),
description: Some("Verbosity level".to_string()),
..Default::default()
},
]),
subcommands: Some(subcommands),
}),
},
build: BuildConfig {
dev_path: String::from("http://localhost:4000"),
dev_path: String::from("../dist"),
},
}
}
@ -196,6 +345,7 @@ mod test {
host: String::from("http://127.0.0.1"),
port: String::from("random"),
},
cli: None,
};
// create a build config

View File

@ -4,6 +4,7 @@
)]
pub mod command;
pub mod config;
pub mod dialog;
pub mod dir;
pub mod file;
@ -13,6 +14,12 @@ pub mod rpc;
pub mod tcp;
pub mod version;
#[cfg(feature = "cli")]
pub mod cli;
#[cfg(feature = "cli")]
#[macro_use]
extern crate clap;
pub use tauri_utils::*;
pub use anyhow::Result;

View File

@ -39,6 +39,7 @@ tauri = {path = ".", features = [ "all-api", "edge" ]}
serde = { version = "1.0", features = [ "derive" ] }
[features]
cli = ["tauri-api/cli"]
edge = ["web-view/edge"]
embedded-server = ["tiny_http"]
no-server = []
@ -66,4 +67,3 @@ features = ["all-api"]
[[example]]
name = "communication"
path = "examples/communication/src-tauri/src/main.rs"

View File

@ -0,0 +1,5 @@
document.getElementById('cli-matches').addEventListener('click', function () {
window.tauri.cliMatches()
.then(registerResponse)
.catch(registerResponse)
})

View File

@ -1,279 +1,312 @@
<!DOCTYPE html>
<html>
<head>
<style>
* {
font-family:Arial, Helvetica, sans-serif;
}
body {
background: #889;
}
.logo-container {
width: 95%;
margin: 0px auto;
overflow: hidden;
}
.logo-link {
font-weight: 700;
position: absolute;
top:150px;
right: 10px;
}
<head>
<style>
* {
font-family: Arial, Helvetica, sans-serif;
}
.logo {
width: 32px;
height: 32px;
cursor: pointer;
position: fixed;
z-index: 10;
top:7px;
right: 10px;
}
#response {
position: absolute;
left:10px;
right:10px;
top:440px;
min-height:110px;
background: #aab;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
word-wrap: break-word;
padding: 5px;
border-radius:5px;
overflow-y:auto;
}
body {
background: #889;
}
input, select {
background: white;
font-family: system-ui, sans-serif;
border: 0;
border-radius: 0.25rem;
font-size: 1rem;
line-height: 1.2;
padding: 0.25rem 0.5rem;
margin: 0.25rem;
}
.logo-container {
width: 95%;
margin: 0px auto;
overflow: hidden;
}
button:hover,
button:focus {
background: #0053ba;
}
.logo-link {
font-weight: 700;
position: absolute;
top: 150px;
right: 10px;
}
button:focus {
outline: 1px solid #fff;
outline-offset: -4px;
}
.logo {
width: 32px;
height: 32px;
cursor: pointer;
position: fixed;
z-index: 10;
top: 7px;
right: 10px;
}
button:active {
transform: scale(0.99);
}
.button {
border: 0;
border-radius: 0.25rem;
background: #1E88E5;
color: white;
font-family: system-ui, sans-serif;
font-size: 1rem;
line-height: 1.2;
white-space: nowrap;
text-decoration: none;
padding: 0.25rem 0.5rem;
margin: 0.25rem;
cursor: pointer;
}
.bottom {
position:fixed;
bottom:0;
left:0;
text-align: center;
width: 100%;
padding: 5px;
background: #333;
color: #eef;
}
.dark-link {
color: white;
text-decoration: none!important;
}
#response {
position: absolute;
left: 10px;
right: 10px;
top: 440px;
min-height: 110px;
background: #aab;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
word-wrap: break-word;
padding: 5px;
border-radius: 5px;
overflow-y: auto;
}
.tabs-container {
position:fixed;
height: 400px;
top:20px;
left:10px;
right:10px;
z-index: 9;
}
.tabs {
position: relative;
min-height: 400px;
clear: both;
}
.tab {
float: left;
}
.tab > label {
background: #eee;
padding: 10px;
border: 1px solid transparent;
margin-left: -1px;
position: relative;
left: 1px;
}
.tabs > .tabber {
border-top-left-radius: 5px;
}
.tabs > .tabber ~ .tabber {
border-top-left-radius: none;
}
.tab [type=radio] {
display: none;
}
.content {
position: absolute;
top: 28px;
left: 0;
background: #bbc;
right: 0;
bottom: 0;
padding: 20px;
border: 1px solid transparent;
border-top-right-radius: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
[type=radio]:checked ~ label {
background: #bbc;
border-bottom: 1px solid transparent;
z-index: 2;
}
[type=radio]:checked ~ label ~ .content {
z-index: 1;
}
</style>
</head>
<body>
<div class="logo-container">
<img src="icon.png" class="logo">
</div>
input,
select {
background: white;
font-family: system-ui, sans-serif;
border: 0;
border-radius: 0.25rem;
font-size: 1rem;
line-height: 1.2;
padding: 0.25rem 0.5rem;
margin: 0.25rem;
}
<div class="tabs-container">
<div class="tabs">
<div class="tab">
<input type="radio" id="tab-1" name="tab-group-1" checked>
<label class="tabber" for="tab-1">Messages</label>
<div class="content">
<button class="button" id="log">Call Log API</button>
<button class="button" id="request">Call Request (async) API</button>
<button class="button" id="event">Send event to Rust</button>
button:hover,
button:focus {
background: #0053ba;
}
<div style="margin-top: 24px">
<input id="title" value="Awesome Tauri Example!">
<button class="button" id="set-title">Set title</button>
</div>
button:focus {
outline: 1px solid #fff;
outline-offset: -4px;
}
button:active {
transform: scale(0.99);
}
.button {
border: 0;
border-radius: 0.25rem;
background: #1E88E5;
color: white;
font-family: system-ui, sans-serif;
font-size: 1rem;
line-height: 1.2;
white-space: nowrap;
text-decoration: none;
padding: 0.25rem 0.5rem;
margin: 0.25rem;
cursor: pointer;
}
.bottom {
position: fixed;
bottom: 0;
left: 0;
text-align: center;
width: 100%;
padding: 5px;
background: #333;
color: #eef;
}
.dark-link {
color: white;
text-decoration: none !important;
}
.tabs-container {
position: fixed;
height: 400px;
top: 20px;
left: 10px;
right: 10px;
z-index: 9;
}
.tabs {
position: relative;
min-height: 400px;
clear: both;
}
.tab {
float: left;
}
.tab>label {
background: #eee;
padding: 10px;
border: 1px solid transparent;
margin-left: -1px;
position: relative;
left: 1px;
}
.tabs>.tabber {
border-top-left-radius: 5px;
}
.tabs>.tabber~.tabber {
border-top-left-radius: none;
}
.tab [type=radio] {
display: none;
}
.content {
position: absolute;
top: 28px;
left: 0;
background: #bbc;
right: 0;
bottom: 0;
padding: 20px;
border: 1px solid transparent;
border-top-right-radius: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
[type=radio]:checked~label {
background: #bbc;
border-bottom: 1px solid transparent;
z-index: 2;
}
[type=radio]:checked~label~.content {
z-index: 1;
}
</style>
</head>
<body>
<div class="logo-container">
<img src="icon.png" class="logo">
</div>
<div class="tabs-container">
<div class="tabs">
<div class="tab">
<input type="radio" id="tab-1" name="tab-group-1" checked>
<label class="tabber" for="tab-1">Messages</label>
<div class="content">
<button class="button" id="log">Call Log API</button>
<button class="button" id="request">Call Request (async) API</button>
<button class="button" id="event">Send event to Rust</button>
<div style="margin-top: 24px">
<input id="title" value="Awesome Tauri Example!">
<button class="button" id="set-title">Set title</button>
</div>
</div>
<div class="tab">
<input type="radio" id="tab-2" name="tab-group-1">
<label class="tabber" for="tab-2">File System</label>
<div class="content">
<div style="margin-top: 24px">
<select class="button" id="dir">
<option value="">None</option>
</select>
<input id="path-to-read" placeholder="Type the path to read...">
<button class="button" id="read">Read</button>
</div>
<div class="tab">
<input type="radio" id="tab-2" name="tab-group-1">
<label class="tabber" for="tab-2">File System</label>
<div class="content">
<div style="margin-top: 24px">
<select class="button" id="dir">
<option value="">None</option>
</select>
<input id="path-to-read" placeholder="Type the path to read...">
<button class="button" id="read">Read</button>
</div>
<div style="margin-top: 24px">
<input id="dialog-default-path" placeholder="Default path">
<input id="dialog-filter" placeholder="Extensions filter">
<div>
<input type="checkbox" id="dialog-multiple">
<label>Multiple</label>
</div>
<div>
<input type="checkbox" id="dialog-directory">
<label>Directory</label>
</div>
<div style="margin-top: 24px">
<input id="dialog-default-path" placeholder="Default path">
<input id="dialog-filter" placeholder="Extensions filter">
<div>
<input type="checkbox" id="dialog-multiple">
<label>Multiple</label>
</div>
<div>
<input type="checkbox" id="dialog-directory">
<label>Directory</label>
</div>
<button class="button" id="open-dialog">Open dialog</button>
<button class="button" id="save-dialog">Open save dialog</button>
</div>
<button class="button" id="open-dialog">Open dialog</button>
<button class="button" id="save-dialog">Open save dialog</button>
</div>
</div>
</div>
<div class="tab">
<input type="radio" id="tab-3" name="tab-group-1">
<label class="tabber" for="tab-3">Communication</label>
<div class="content">
<div style="margin-top: 24px">
<input id="url" value="https://tauri.studio">
<button class="button" id="open-url">Open URL</button>
</div>
<div class="tab">
<input type="radio" id="tab-3" name="tab-group-1">
<label class="tabber" for="tab-3">Communication</label>
<div class="content">
<div style="margin-top: 24px">
<input id="url" value="https://tauri.studio">
<button class="button" id="open-url">Open URL</button>
</div>
<div style="margin-top: 24px">
<select class="button" id="request-method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<input id="request-url" placeholder="Type the request URL...">
<br/>
<textarea id="request-body" placeholder="Request body" rows="5" style="width:100%;margin-right:10px;font-size:12px"></textarea>
</br>
<button class="button" id="make-request">Make request</button>
</div>
<div style="margin-top: 24px">
<select class="button" id="request-method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<input id="request-url" placeholder="Type the request URL...">
<br />
<textarea id="request-body" placeholder="Request body" rows="5"
style="width:100%;margin-right:10px;font-size:12px"></textarea>
</br>
<button class="button" id="make-request">Make request</button>
</div>
</div>
</div>
<div class="tab">
<input type="radio" id="tab-4" name="tab-group-1">
<label class="tabber" for="tab-4">CLI</label>
<div class="content">
<div style="margin-top: 24px">
<button class="button" id="cli-matches">Get matches</button>
</div>
</div>
</div>
</div>
<div id="response"></div>
<div class="bottom">
<a class="dark-link" target="_blank" href="https://tauri.studio">Tauri Documentation</a>&nbsp;&nbsp;&nbsp;
<a class="dark-link" target="_blank" href="https://github.com/tauri-apps/tauri">Github Repo</a>&nbsp;&nbsp;&nbsp;
<a class="dark-link" target="_blank" href="https://github.com/tauri-apps/tauri/tree/dev/tauri/examples/communication">Source for this App</a>
</div>
<script>
function registerResponse (response) {
document.getElementById('response').innerHTML = typeof response === 'object'
? JSON.stringify(response)
: response
}
</div>
<div id="response"></div>
<div class="bottom">
<a class="dark-link" target="_blank" href="https://tauri.studio">Tauri Documentation</a>&nbsp;&nbsp;&nbsp;
<a class="dark-link" target="_blank" href="https://github.com/tauri-apps/tauri">Github Repo</a>&nbsp;&nbsp;&nbsp;
<a class="dark-link" target="_blank"
href="https://github.com/tauri-apps/tauri/tree/dev/tauri/examples/communication">Source for this App</a>
</div>
<script>
function registerResponse(response) {
document.getElementById('response').innerHTML = typeof response === 'object' ?
JSON.stringify(response) :
response
}
function addClickEnterHandler (button, input, handler) {
button.addEventListener('click', handler)
input.addEventListener('keyup', function (e) {
if (e.keyCode === 13) {
handler()
}
})
}
window.tauri.listen('rust-event', function (res) {
document.getElementById('response').innerHTML = JSON.stringify(res)
function addClickEnterHandler(button, input, handler) {
button.addEventListener('click', handler)
input.addEventListener('keyup', function (e) {
if (e.keyCode === 13) {
handler()
}
})
}
document.querySelector('.logo').addEventListener('click', function () {
window.tauri.open('https://tauri.studio/')
})
window.tauri.listen('rust-event', function (res) {
document.getElementById('response').innerHTML = JSON.stringify(res)
})
document.querySelector('.logo').addEventListener('click', function () {
window.tauri.open('https://tauri.studio/')
})
var dirSelect = document.getElementById('dir')
for (var key in window.tauri.Dir) {
var value = window.tauri.Dir[key]
var opt = document.createElement("option")
opt.value = value
opt.innerHTML = key
dirSelect.appendChild(opt)
}
</script>
<script src="communication.js"></script>
<script src="fs.js"></script>
<script src="window.js"></script>
<script src="dialog.js"></script>
<script src="http.js"></script>
<script src="cli.js"></script>
</body>
var dirSelect = document.getElementById('dir')
for (var key in window.tauri.Dir) {
var value = window.tauri.Dir[key]
var opt = document.createElement("option")
opt.value = value
opt.innerHTML = key
dirSelect.appendChild(opt)
}
</script>
<script src="communication.js"></script>
<script src="fs.js"></script>
<script src="window.js"></script>
<script src="dialog.js"></script>
<script src="http.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -24,7 +24,7 @@ icon = [
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = [ "derive" ] }
tauri = { path = "../../..", features = [ "all-api", "edge" ] }
tauri = { path = "../../..", features = [ "all-api", "edge", "cli" ] }
[target."cfg(windows)".build-dependencies]
winres = "0.1"

View File

@ -5,6 +5,43 @@
},
"ctx": {},
"tauri": {
"cli": {
"description": "Tauri communication example",
"longDescription": null,
"beforeHelp": null,
"afterHelp": null,
"args": [{
"short": "c",
"name": "config",
"takesValue": true,
"description": "Config path"
}, {
"short": "t",
"name": "theme",
"takesValue": true,
"description": "App theme",
"possibleValues": ["light", "dark", "system"]
}, {
"short": "v",
"name": "verbose",
"multipleOccurrences": true,
"description": "Verbosity level"
}],
"subcommands": {
"update": {
"description": "Updates the app",
"longDescription": null,
"beforeHelp": null,
"afterHelp": null,
"args": [{
"short": "b",
"name": "background",
"description": "Update in background"
}],
"subcommands": null
}
}
},
"embeddedServer": {
"active": false
},
@ -31,4 +68,4 @@
"active": true
}
}
}
}

View File

@ -570,6 +570,15 @@ window.tauri = {
asset: assetName,
assetType: assetType || 'unknown'
})
},
cliMatches: function () {
return this.promisified({
cmd: 'cliMatches'
})
}
};

View File

@ -12,13 +12,22 @@ use web_view::{builder, Content, WebView};
use super::App;
#[cfg(feature = "embedded-server")]
use crate::api::tcp::{get_available_port, port_is_available};
use crate::config::{get, Config};
use tauri_api::config::{get, Config};
#[cfg(feature = "cli")]
use tauri_api::cli::get_matches;
// Main entry point function for running the Webview
pub(crate) fn run(application: &mut App) -> crate::Result<()> {
// get the tauri config struct
let config = get()?;
#[cfg(feature = "cli")]
{
let matches = get_matches(config.clone());
crate::cli::set_matches(matches)?;
}
// setup the content using the config struct depending on the compile target
let main_content = setup_content(config.clone())?;
@ -72,7 +81,10 @@ fn setup_content(config: Config) -> crate::Result<Content<String>> {
let dev_dir = config.build.dev_path;
let dev_path = Path::new(&dev_dir).join("index.tauri.html");
if !dev_path.exists() {
panic!("Couldn't find 'index.tauri.html' inside {}; did you forget to run 'tauri dev'?", dev_dir);
panic!(
"Couldn't find 'index.tauri.html' inside {}; did you forget to run 'tauri dev'?",
dev_dir
);
}
Ok(Content::Html(read_to_string(dev_path)?))
}
@ -280,8 +292,8 @@ mod test {
#[cfg(not(feature = "embedded-server"))]
use std::{env, fs::read_to_string, path::Path};
fn init_config() -> crate::config::Config {
crate::config::get().expect("unable to setup default config")
fn init_config() -> tauri_api::config::Config {
tauri_api::config::get().expect("unable to setup default config")
}
#[test]
@ -289,6 +301,15 @@ mod test {
let config = init_config();
let _c = config.clone();
let tauri_dir = match option_env!("TAURI_DIR") {
Some(d) => d.to_string(),
None => env::current_dir()
.unwrap()
.into_os_string()
.into_string()
.expect("Unable to convert to normal String"),
};
env::set_current_dir(tauri_dir).expect("failed to change cwd");
let res = super::setup_content(config);
#[cfg(feature = "embedded-server")]
@ -320,17 +341,11 @@ mod test {
match res {
Ok(Content::Url(dp)) => assert_eq!(dp, _c.build.dev_path),
Ok(Content::Html(s)) => {
let dist_dir = match option_env!("TAURI_DIST_DIR") {
Some(d) => d.to_string(),
None => env::current_dir()
.unwrap()
.into_os_string()
.into_string()
.expect("Unable to convert to normal String"),
};
let dev_dir = _c.build.dev_path;
let dev_path = Path::new(&dev_dir).join("index.tauri.html");
assert_eq!(
s,
read_to_string(Path::new(&dist_dir).join("index.tauri.html")).unwrap()
read_to_string(dev_path).expect("failed to read dev path")
);
}
_ => assert!(false),

14
tauri/src/cli.rs Normal file
View File

@ -0,0 +1,14 @@
use once_cell::sync::OnceCell;
use tauri_api::cli::Matches;
static MATCHES: OnceCell<Matches> = OnceCell::new();
pub(crate) fn set_matches(matches: Matches) -> crate::Result<()> {
MATCHES
.set(matches)
.map_err(|_| anyhow::anyhow!("failed to set once_cell matches"))
}
pub fn get_matches() -> Option<&'static Matches> {
MATCHES.get()
}

View File

@ -178,6 +178,16 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
} => {
load_asset(webview, asset, asset_type, callback, error)?;
}
#[cfg(feature = "cli")]
CliMatches { callback, error } => crate::execute_promise(
webview,
move || match crate::cli::get_matches() {
Some(matches) => Ok(serde_json::to_string(matches)?),
None => Err(anyhow::anyhow!(r#""failed to get matches""#)),
},
callback,
error,
),
}
Ok(())
}

View File

@ -160,4 +160,9 @@ pub enum Cmd {
callback: String,
error: String,
},
#[cfg(feature = "cli")]
CliMatches {
callback: String,
error: String,
},
}

View File

@ -41,10 +41,7 @@ pub fn save<T: 'static>(
) {
crate::execute_promise_sync(
webview,
move || {
save_file(options.filter, options.default_path)
.map(map_response)
},
move || save_file(options.filter, options.default_path).map(map_response),
callback,
error,
);

View File

@ -12,12 +12,12 @@ pub fn make_request<T: 'static>(
webview,
move || {
let response_type = options.response_type.clone();
request(options).map(|response| {
match response_type.unwrap_or(ResponseType::Json) {
request(options).map(
|response| match response_type.unwrap_or(ResponseType::Json) {
ResponseType::Text => format!(r#""{}""#, response),
_ => response,
}
})
},
)
},
callback,
error,

View File

@ -3,8 +3,8 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use lazy_static::lazy_static;
use web_view::Handle;
use once_cell::sync::Lazy;
use web_view::Handle;
struct EventHandler {
on_event: Box<dyn FnMut(Option<String>) + Send>,
@ -74,10 +74,8 @@ pub fn on_event(event: String, data: Option<String>) {
.lock()
.expect("Failed to lock listeners: on_event()");
let key = event.clone();
if l.contains_key(&key) {
let handler = l.get_mut(&key).expect("Failed to get mutable handler");
if l.contains_key(&event) {
let handler = l.get_mut(&event).expect("Failed to get mutable handler");
(handler.on_event)(data);
}
}
@ -151,4 +149,4 @@ mod test {
assert!(l.contains_key(&key));
}
}
}
}

View File

@ -5,11 +5,13 @@
#[cfg(any(feature = "embedded-server", feature = "no-server"))]
pub mod assets;
pub mod config;
pub mod event;
#[cfg(feature = "embedded-server")]
pub mod server;
#[cfg(feature = "cli")]
pub mod cli;
mod app;
mod endpoints;
#[allow(dead_code)]