Skip to content
This repository was archived by the owner on Mar 10, 2026. It is now read-only.

Commit 76170ec

Browse files
committed
feat(tauri): add android support and splash screen tauri plugin (WIP)
1 parent 8192ebd commit 76170ec

51 files changed

Lines changed: 1524 additions & 80 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ crash.log
2525

2626
# Tauri
2727
/src-tauri/target/
28-
/src-tauri/gen/
28+
/src-tauri/gen/
29+
30+
# Worktrees
31+
.worktrees

crates/sable-macros/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "sable-macros"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
proc-macro2 = "1"
11+
quote = "1"
12+
syn = { version = "2", features = ["full"] }

crates/sable-macros/src/lib.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use proc_macro::TokenStream;
2+
use proc_macro2::TokenStream as TokenStream2;
3+
use quote::quote;
4+
use syn::{
5+
parse::{Parse, ParseStream},
6+
parse_macro_input, Attribute, Path, Token,
7+
};
8+
9+
struct CommandItem {
10+
/// The tokens inside `#[cfg(...)]`, e.g. `desktop` or `target_os = "windows"`.
11+
/// `None` means the command is always compiled in.
12+
cfg_tokens: Option<TokenStream2>,
13+
path: Path,
14+
}
15+
16+
struct CommandList(Vec<CommandItem>);
17+
18+
impl Parse for CommandList {
19+
fn parse(input: ParseStream) -> syn::Result<Self> {
20+
let mut items = vec![];
21+
while !input.is_empty() {
22+
let attrs = Attribute::parse_outer(input)?;
23+
let path: Path = input.parse()?;
24+
25+
// Extract the first #[cfg(...)] attribute if present.
26+
// Any other attributes are ignored (they wouldn't make sense here anyway).
27+
let cfg_tokens = attrs.iter().find_map(|attr| {
28+
if !attr.path().is_ident("cfg") {
29+
return None;
30+
}
31+
attr.meta
32+
.require_list()
33+
.ok()
34+
.map(|list| list.tokens.clone())
35+
});
36+
37+
items.push(CommandItem { cfg_tokens, path });
38+
39+
// Consume optional trailing comma
40+
if input.peek(Token![,]) {
41+
let _ = input.parse::<Token![,]>();
42+
}
43+
}
44+
Ok(CommandList(items))
45+
}
46+
}
47+
48+
/// A drop-in replacement for `tauri_specta::collect_commands!` that supports
49+
/// `#[cfg(...)]` attributes on individual commands.
50+
///
51+
/// # Example
52+
/// ```rust
53+
/// collect_commands![
54+
/// #[cfg(desktop)]
55+
/// desktop_tray::set_close_to_tray_enabled,
56+
/// windows::snap_overlay::show_snap_overlay,
57+
/// windows::snap_overlay::hide_snap_overlay,
58+
/// ]
59+
/// ```
60+
///
61+
/// # How it works
62+
///
63+
/// For each unique cfg predicate P found in the list the macro generates two
64+
/// complete `tauri_specta::internal::command(generate_handler![...],
65+
/// collect_functions![...])` calls — one for `#[cfg(P)]` (including those
66+
/// commands) and one for `#[cfg(not(P))]` (excluding them). The compiler
67+
/// picks exactly one branch per target, so every command path only needs to
68+
/// exist on the targets where its cfg condition is true.
69+
///
70+
/// For N distinct predicates, 2^N branches are emitted. In practice only
71+
/// `#[cfg(desktop)]` is used so this is always just two branches.
72+
#[proc_macro]
73+
pub fn collect_commands(input: TokenStream) -> TokenStream {
74+
let CommandList(items) = parse_macro_input!(input as CommandList);
75+
76+
// Collect the unique cfg predicates present in this invocation.
77+
let mut predicates: Vec<TokenStream2> = vec![];
78+
for item in &items {
79+
if let Some(cfg) = &item.cfg_tokens {
80+
let key = cfg.to_string();
81+
if !predicates
82+
.iter()
83+
.any(|p: &TokenStream2| p.to_string() == key)
84+
{
85+
predicates.push(cfg.clone());
86+
}
87+
}
88+
}
89+
90+
let n = predicates.len();
91+
let num_variants = 1usize << n; // 2^n — always at least 1
92+
93+
let mut branches: Vec<TokenStream2> = vec![];
94+
95+
for variant in 0..num_variants {
96+
// For variant `v`, bit `i` being set means predicate[i] is "active"
97+
// (true) for this branch.
98+
99+
// Build `#[cfg(all(pred0_or_not, pred1_or_not, ...))]`
100+
let conditions: Vec<TokenStream2> = predicates
101+
.iter()
102+
.enumerate()
103+
.map(|(i, pred)| {
104+
if variant & (1 << i) != 0 {
105+
quote! { #pred }
106+
} else {
107+
quote! { not(#pred) }
108+
}
109+
})
110+
.collect();
111+
112+
let cfg_guard: TokenStream2 = if conditions.is_empty() {
113+
// No predicates at all — unconditional (wrapping in all() is valid).
114+
quote! {}
115+
} else {
116+
quote! { #[cfg(all(#(#conditions),*))] }
117+
};
118+
119+
// Collect commands that are visible in this variant:
120+
// - always-on commands (no cfg attribute) are always included
121+
// - cfg-gated commands are included only when their predicate bit is set
122+
let variant_paths: Vec<&Path> = items
123+
.iter()
124+
.filter(|item| match &item.cfg_tokens {
125+
None => true, // always-on
126+
Some(cfg) => {
127+
let key = cfg.to_string();
128+
let idx = predicates
129+
.iter()
130+
.position(|p| p.to_string() == key)
131+
.unwrap();
132+
variant & (1 << idx) != 0
133+
}
134+
})
135+
.map(|item| &item.path)
136+
.collect();
137+
138+
branches.push(quote! {
139+
#cfg_guard
140+
let __commands = ::tauri_specta::internal::command(
141+
::tauri::generate_handler![#(#variant_paths),*],
142+
::specta::function::collect_functions![#(#variant_paths),*],
143+
);
144+
});
145+
}
146+
147+
quote! {
148+
{
149+
#(#branches)*
150+
__commands
151+
}
152+
}
153+
.into()
154+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/.vs
2+
.DS_Store
3+
.Thumbs.db
4+
*.sublime*
5+
.idea/
6+
debug.log
7+
package-lock.json
8+
.vscode/settings.json
9+
yarn.lock
10+
11+
/.tauri
12+
/.gradle
13+
/target
14+
Cargo.lock
15+
node_modules/
16+
17+
dist-js
18+
dist
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "tauri-plugin-splashscreen"
3+
version = "0.1.0"
4+
authors = [ "You" ]
5+
description = ""
6+
edition = "2021"
7+
rust-version = "1.77.2"
8+
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
9+
links = "tauri-plugin-splashscreen"
10+
11+
[dependencies]
12+
tauri = { version = "2.10.3" }
13+
serde = "1.0"
14+
thiserror = "2"
15+
16+
[build-dependencies]
17+
tauri-plugin = { version = "2.5.4", features = ["build"] }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Tauri Plugin splashscreen
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/build
2+
/.tauri
3+
/.gradle
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
plugins {
2+
id("com.android.library")
3+
id("org.jetbrains.kotlin.android")
4+
}
5+
6+
android {
7+
namespace = "moe.sable.app.plugin.splashscreen"
8+
compileSdk = 36
9+
10+
defaultConfig {
11+
minSdk = 21
12+
13+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
14+
consumerProguardFiles("consumer-rules.pro")
15+
}
16+
17+
buildTypes {
18+
release {
19+
isMinifyEnabled = false
20+
proguardFiles(
21+
getDefaultProguardFile("proguard-android-optimize.txt"),
22+
"proguard-rules.pro"
23+
)
24+
}
25+
}
26+
compileOptions {
27+
sourceCompatibility = JavaVersion.VERSION_1_8
28+
targetCompatibility = JavaVersion.VERSION_1_8
29+
}
30+
kotlinOptions {
31+
jvmTarget = "1.8"
32+
}
33+
}
34+
35+
dependencies {
36+
37+
implementation("androidx.core:core-ktx:1.9.0")
38+
implementation("androidx.core:core-splashscreen:1.2.0")
39+
implementation("androidx.appcompat:appcompat:1.6.0")
40+
implementation("com.google.android.material:material:1.7.0")
41+
testImplementation("junit:junit:4.13.2")
42+
androidTestImplementation("androidx.test.ext:junit:1.1.5")
43+
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
44+
implementation(project(":tauri-android"))
45+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
pluginManagement {
2+
repositories {
3+
mavenCentral()
4+
gradlePluginPortal()
5+
google()
6+
}
7+
resolutionStrategy {
8+
eachPlugin {
9+
switch (requested.id.id) {
10+
case "com.android.library":
11+
useVersion("8.0.2")
12+
break
13+
case "org.jetbrains.kotlin.android":
14+
useVersion("1.8.20")
15+
break
16+
}
17+
}
18+
}
19+
}
20+
21+
dependencyResolutionManagement {
22+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
23+
repositories {
24+
mavenCentral()
25+
google()
26+
27+
}
28+
}
29+
30+
include ':tauri-android'
31+
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')

0 commit comments

Comments
 (0)