digital consciousness spans platforms: I

Table of contents

The manner in which we consume digital information spans platforms, i.e. web browsers, desktop applications, mobile applications, digital watches, smart TVs, gaming consoles, etc.
You must have often learned that so and so software or so and so game is available in desktop, browser and mobile applications. I for one, acknowledge that I use X(Twitter) on both my phone and on the browser in my computer. Take YouTube, Facebook, Instagram, Twitch, Netflix, Discord,…. and list goes possibly endless.
With advancing technology, people interact with multiple machines for productivity and entertainment, which is perfectly evident with the existence of so many versions of OS dedicated to such machines, be it, desktopOS, mobileOS, tvOS, watchOS, gamingOS, handheldOS, carOS (yes, this is a booming industry, as you can probably imagine, there is a venue of comfort and accessibility for the user and mounts of information around the usage of the car, which can be organized and monetized), etc.
Special OS’
Lets head down to basics first.
An OS is primarily a way to converse with the machine and help the machine better manage its resources and to better serve the user.
Its two primary jobs are:
Hardware Abstraction & Resource Management: It presents complex hardware in a simplified, consistent way for applications and manages contention for those resources (e.g., deciding which app gets CPU time, how memory is allocated, who can access the camera).
User Interface (UI): It provides the environment through which a user interacts with the device, whether that's with a graphical interface (icons, windows), a voice assistant, or a simple remote control.
Keep this information in context when we discuss the specialized OSs for digital machines.
1. Mobile Operating Systems (e.g., Android, iOS)
These run on smartphones and tablets, devices that are personal, portable, and always connected.
The "Conversation": The user converses primarily through a high-resolution touchscreen. The device is a window into their digital life, used for communication, productivity, and entertainment on the go.
What Makes it Special (Key Design Priorities):
Touch-First Interface: The entire OS is built around direct manipulation with fingers—taps, swipes, pinches. This is fundamentally different from a mouse-and-keyboard paradigm.
Aggressive Power Management: Battery life is paramount. The OS is ruthless about putting apps to sleep, limiting background processes, and shutting down hardware components (like GPS or Wi-Fi) when not in use.
Connectivity Management: It must seamlessly juggle Wi-Fi, Cellular (5G/LTE), and Bluetooth without user intervention. A poor connection should degrade gracefully, not crash an app.
App Sandboxing & Security: Since users download countless apps from various developers, the OS must enforce a strict security model. Each app runs in its own "sandbox", with limited access to the system and other apps' data, requiring explicit user permission for things like camera or location access.
Notifications: The OS needs a robust system to deliver timely, actionable, and non-intrusive alerts, as this is a primary way apps engage the user.
2. Wearable Operating Systems (e.g., watchOS, Wear OS)
These run on smartwatches and fitness trackers, devices defined by their small size and intimate placement on the user's body.
The "Conversation": The conversation is meant to be brief and contextual. The user interacts through a tiny screen, voice commands, and physical buttons/crowns, often just for a few seconds at a time.
What Makes it Special (Key Design Priorities):
"Glanceability": Information must be digestible in a 1-3 second glance. UIs are minimal, often presented as
complications
(small widgets on a watch face). Long, complex interactions are a design failure.Extreme Power Efficiency: With tiny batteries, the OS must be incredibly efficient. Screen-on time is measured in seconds, and background activity is severely restricted.
Sensor Integration: The OS is built to constantly and efficiently manage data from intimate sensors like heart rate monitors, accelerometers, and blood oxygen sensors.
Tight Phone Symbiosis: A watch OS is not a standalone entity. It's designed to be a satellite of the phone, offloading heavy computation and relying on the phone for connectivity and configuration. Notifications are a primary function, but they are relayed from the phone.
3. Smart TV & Set-Top Box Operating Systems (e.g., tvOS, Android TV/Google TV, webOS, Tizen)
These power our living room entertainment, where the user is sitting far away and focused on consuming content.
The "Conversation": The user is relaxed, leaning back, and using a simple remote control (D-pad, voice, or a basic pointer). The primary goal is finding and playing media.
What Makes it Special (Key Design Priorities):
The "10-Foot UI": All UI elements—text, icons, buttons—must be large, clear, and legible from a distance of about 10 feet (3 meters). High information density is bad; clarity is good.
Remote-Control Navigation: The entire OS must be navigable with up, down, left, right, and select. This focus on directional pads (D-pads) dictates the grid-like layout of most TV UIs.
Media Playback is King: The OS prioritizes giving resources to the video/audio pipeline. It must support a wide array of codecs (
H.264
,HEVC
,AV1
) and digital rights management (DRM
) schemes, and ensure smooth, high-resolution playback above all else.Content Discovery: The home screen of a TV OS is not about launching apps; it's about surfacing content from within those apps. The OS's job is to aggregate and recommend shows and movies to keep the user engaged.
4. Gaming Console Operating Systems (e.g., PlayStation's System Software, Xbox OS, Horizon)
These are highly specialized operating systems designed for one primary task: playing games at the highest possible performance.
The "Conversation": The user interacts via a high-precision gamepad for immersive, low-latency gaming. The OS UI is secondary and used mainly for launching games and managing social connections.
What Makes it Special (Key Design Priorities):
Maximum Performance from Fixed Hardware: Unlike a PC, a console has a fixed hardware spec. The OS is a thin, highly-optimized layer designed to get out of the way and give a game near "bare metal" access to the GPU and CPU. Resource management is about giving everything to the game.
Low-Level Graphics APIs: The OS exposes powerful, custom graphics APIs (like
GNM/GNMX
on PlayStation orDirectX 12 Ultimate
on Xbox) that allow developers to squeeze every drop of performance from the hardware.Fast I/O and Asset Streaming: With modern games having massive worlds, the OS is optimized for ultra-fast loading from NVMe SSDs, allowing games to stream assets into memory in real-time.
Integrated Social/Storefront Layer: The OS is deeply intertwined with the platform's social network (friends lists, parties, achievements) and digital store. These are not separate apps but core OS features.
The common thread is that the device's purpose dictates the OS' priorities. When you write an application for a platform, you are not just writing code; you are entering into a contract with its OS, agreeing to abide by its rules on power management, user interaction, and data access. This is the fundamental challenge of multi-platform development.
Two-fold approach
Lets consider a basic example of a service you want to provide to the user, across platforms. Obviously there are tools and applications available to make this easier and more streamlined for you. But, as a thought experiment, what concepts and factors come to mind when you want to design something supposedly for multiple platforms ..??
Of course, defining the scope of the requirement and the application is half the solution, so you have your basic logic setup, i.e. what is going to be the flow of information and intelligence, what elements are going to be exposed to the end users, how will the application modules be segregated, basically, everything high level at first.
Then you delve into the implementation, i.e. keeping note of the basics such as i/o performance, specific optimizations if possible, concurrency models, creating runtime modules and/or executables which can be shared, os specific calls, permissions issues if any, app security, app size, file handling, memory management, etc.
To structure a robust and maintainable multi-platform application, we can't think of it as one monolithic program. We must split it into two logical parts:
The Shared Core (or "Engine"): This is the heart of our application. It's platform-agnostic and contains all the logic that should be identical everywhere.
The Platform-Specific Shell (or "Adapter/Cockpit"): This is the layer that "adapts" the Core to each specific OS. It handles everything the Core cannot and should not know about, like how to draw a button or ask for camera permission.
Designing the Shared Core (The Engine)
This is the code we want to write once and share everywhere. Our primary goal here is portability and consistency.
Shared Code & Runtime Modules
How to Share? We need to write the Core in a highly portable language. C++ is the classic choice because it compiles almost anywhere and offers fine-grained control. Rust is a modern, safer alternative. We would compile this Core into a shared library: a
.so
file for Android, a.framework
or.dylib
for iOS/macOS, and a.dll
for Windows.What's Inside?
Business Logic: The rules of our app. How to create, edit, and delete a note. The logic for searching and filtering by tags.
Data Models: The Note struct/class, the Tag struct, etc.
Cryptography: The encryption/decryption algorithms. This must be in the Core to ensure security is consistent and not re-implemented (and potentially flawed) on each platform.
Network Layer: Logic for making API calls to our sync server, handling JSON serialization/deserialization, and managing server responses.
Memory Management
The Challenge: If we use C++, we are responsible for our own memory. The iOS (ARC) and Android (JVM Garbage Collector) memory managers won't manage our C++ objects.
Our Design: It is better to stay disciplined. Opt for modern C++ practices like RAII (Resource Acquisition Is Initialization) and smart pointers (
std::unique_ptr
,std::shared_ptr
) to prevent memory leaks within the Core. The Core manages its own heap, and we must provide clean functions for the Shell to allocate and free Core objects.
File Handling & I/O Performance
The Challenge: The Core needs to save a note to disk, but it has no concept of a file system path.
/Users/john/Documents/
on macOS is meaningless on Android's sandboxed storage (/data/data/com.myapp.securenotes/files/
).Our Design: We use an abstraction.
The Core defines an interface, not an implementation. For example, a C++ function
core_saveFile(filename, data)
.This function doesn't actually write the file. It makes a call across the "bridge" to the Platform Shell.
The Shell receives this request and uses the platform's native, optimized I/O APIs to write the data to the correct, sandboxed location.
This is a critical pattern: The Core dictates what to do, and the Shell dictates how and where to do it.
Concurrency Models
The Challenge: Our app needs to perform long-running tasks without freezing the UI (e.g., syncing notes in the background). Each OS has its own preferred way of doing this (
Grand Central Dispatch
on iOS,Coroutines/WorkManager
on Android).Our Design: Similar to file handling, we use abstraction. The Core can have its own thread pool for CPU-intensive tasks like encrypting a large batch of notes. However, for tasks that need to survive the app being closed, the Core must request this from the Shell. It would call a function like core_scheduleBackgroundTask(), and the Shell would implement this using WorkManager or BGTaskScheduler respectively. This respects the OS's rules for background execution and battery life.
Designing the Platform-Specific Shell (The Adapter)
This is the code we must write for each target platform (e.g., in Swift/Objective-C
for iOS
, Kotlin/Java
for Android
, C#
for Windows
). Its job is to be the perfect, native citizen of its OS.
The "Bridge" and OS-Specific Calls
The Challenge: How does the Swift code on iOS call a function in our C++ Core, and vice versa?
Our Design: We create a "bridge" or
Foreign Function Interface (FFI)
.Shell -> Core: On iOS, we'd use an Objective-C++ "bridging header" to expose our C++ functions to Swift. On Android, we'd use the Java Native Interface (JNI). The Kotlin code calls a Java method, which in turn calls the C++ native code.
Core -> Shell: This is trickier. The Core needs to tell the UI "sync is complete." It does this via callbacks. The Shell passes function pointers/delegates to the Core during initialization. When an event occurs, the Core invokes these callbacks, which triggers native code in the Shell.
Permissions and App Security
The Challenge: The Core has no concept of permissions. It can't pop up a dialog asking for access to files or biometrics (Face ID/Fingerprint).
Our Design: This is purely a Shell responsibility.
The user taps a "Secure with Biometrics" button in the native UI.
The Shell uses the native API (
LocalAuthentication
on iOS,BiometricPrompt
on Android) to request permission.Only if the OS grants permission does the Shell then call a function in the Core like core_setNoteEncryptionKeyFromBiometrics().
Security: The Shell is also responsible for using platform-specific secure storage (Keychain on iOS, Keystore on Android) to store the master encryption key. The Core would simply ask the Shell, "give me the key," and the Shell is responsible for securely retrieving it.
UI/UX
- This is the most obvious part of the Shell. The Shell is 100% responsible for the UI. It creates native buttons, lists, and navigation flows that feel right for the platform. A bottom tab bar on iOS, a navigation drawer (historically) on Android. It adheres to the platform's Human Interface Guidelines.
App Size
The Challenge: Our shared Core library adds to the final app size on every platform. A 5MB C++ library will increase the iOS app size by 5MB and the Android app size by 5MB.
Our Design: This is a trade-off we accept for code reuse. We mitigate it by using compiler flags to strip unused symbols and optimize for size (-Os). We must also be mindful of including only what is necessary in the Core.
Factor | Shared Core (Engine) Responsibility | Platform Shell (Adapter) Responsibility |
Business Logic | Define and implement all rules. | Invoke the Core's logic in response to user input. |
UI/UX | None. Has no concept of UI. | Implement 100% of the UI using native components. |
File System | Request "save data" or "load data" via an abstract interface. | Implement the actual file I/O in the platform's sandboxed directory. |
Permissions | None. Cannot ask the user for anything. | Request all permissions (camera, location, biometrics) using OS APIs. |
Concurrency | Can manage its own threads for pure computation. | Integrates with the OS's background task system (WorkManager, etc.). |
Memory | Manages its own heap (e.g., via smart pointers in C++). | Manages its own objects (via ARC/GC) and bridges memory to the Core. |
Security | Implements cryptographic algorithms. | Uses the platform's secure storage (Keychain/Keystore) to hold keys. |
Implementation Snippets
Let's build out an example: a simple but powerful "Image Filter & Watermark" application. This is a great choice because:
It has a computationally intensive part (applying filters).
It has clear user interaction (uploading, clicking buttons, typing text).
The core logic (the image processing algorithms) is inherently portable.
The application will allow a user to:
Upload an image from their computer.
Apply a pre-defined filter (e.g., Grayscale).
Add a text watermark.
Download the resulting image.
Web
We will start with the web, as it's one of the most accessible platforms. Our goal is to architect it from day one with the "Shared Core / Platform Shell" model in mind.
Here's how we'll construct it using React and WebAssembly.
Make sure, you are setting up a rust library, instead of the default binary,cargo new --lib wasm-util # name of the lib
Example Cargo.toml file, notice the declaration of features = ["console"]
, I would urge you figure out why that is..
[package]
name = "wasm-util"
version = "0.1.0"
edition = "2021"
owner = "Aatir Nadim"
[dependencies]
wasm-bindgen = "0.2.100"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies.web-sys]
version = "0.3.77"
features = ["console"]
// Using wasm-bindgen to create the bridge to JavaScript
use wasm_bindgen::prelude::*;
// This is the function JavaScript will call to apply a grayscale filter.
// It takes a byte slice (our raw image data) and returns a new Vec<u8> (the processed data).
#[wasm_bindgen]
pub fn apply_grayscale(image_data: &[u8]) -> Vec<u8> {
let mut processed_data = image_data.to_vec();
// A simple grayscale algorithm: loop through pixels (4 bytes at a time: R,G,B,A)
for pixel in processed_data.chunks_mut(4) {
let gray = (pixel[0] as u32 + pixel[1] as u32 + pixel[2] as u32) / 3;
pixel[0] = gray as u8; // Red
pixel[1] = gray as u8; // Green
pixel[2] = gray as u8; // Blue
// pixel[3] is Alpha, we leave it untouched
}
processed_data
}
// A more complex function that shows passing a string from JS to Rust.
// This would be much more complex in reality (requiring a font-rendering library).
// For this example, we'll just log it to show it works.
#[wasm_bindgen]
pub fn apply_watermark(image_data: &[u8], text: &str) -> Vec<u8> {
// In a real app, we'd use a library like 'rusttype' to draw the `text` onto the `image_data`.
// For now, let's just prove we received the text.
// The `web_sys::console::log_1` function lets our Rust/Wasm code log to the browser console.
web_sys::console::log_1(&format!("Applying watermark: '{}'", text).into());
// Just return the original image data for this simple example.
// IMPORTANT: this function does not contain any logic,
// only its potential implementation is demonstrated
image_data.to_vec()
}
We will use a tool called wasm-pack
. This compiles the Rust code into a highly optimized .wasm
file and, crucially, generates a JavaScript "glue" file (wasm_util.js
) that makes it easy to use our Rust functions in JavaScript.
I’m not sure about other methods, but Vite has a support for referencing wasm files natively. A Javascript
interface is created when we build WebAssembly
code with wasm-pack
.
Note: Keep note that the output directory created by wasm-pack is available inside the react project (at least for vite build. You could also include the external path in the vite’s runtime, but this way is easier).
import React, { useState, useEffect, useRef } from "react";
import "./App.css";
// 1. Import the Wasm module and its initializer
import init, { apply_grayscale, apply_watermark } from "./pkg/wasm_util.js";
function App() {
const [isWasmLoaded, setWasmLoaded] = useState(false);
const [originalImage, setOriginalImage] = useState(null);
const canvasRef = useRef(null);
// 2. Initialize the Wasm module ONCE on component mount
useEffect(() => {
const loadWasm = async () => {
try {
await init(); // This loads the .wasm file
setWasmLoaded(true);
console.log("Wasm module loaded successfully.");
} catch (error) {
console.error("Error loading Wasm module:", error);
}
};
loadWasm();
}, []); // Empty dependency array ensures this runs only once
const handleImageUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
setOriginalImage(img);
drawImageOnCanvas(img);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
};
const drawImageOnCanvas = (img) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
};
// 3. Define the function to call the Wasm grayscale filter
const applyGrayscaleFilter = () => {
if (!originalImage) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
// Get the raw pixel data from the canvas
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Call the Wasm function!
const processedData = apply_grayscale(imageData.data);
// Put the processed data back onto the canvas
const newImageData = new ImageData(
new Uint8ClampedArray(processedData),
canvas.width,
canvas.height
);
ctx.putImageData(newImageData, 0, 0);
};
// 4. Define the function to call the Wasm watermark function
const applyWatermarkFilter = () => {
if (!originalImage) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Call the Wasm function with a string argument
apply_watermark(imageData.data, "Hello from React!");
// Note: The Rust function currently just logs to the console
// and returns the original data. No visual change will happen.
console.log("Check the browser console for the message from Wasm.");
};
const resetImage = () => {
if (originalImage) {
drawImageOnCanvas(originalImage);
}
};
return (
<div className="App">
<header className="App-header">
<h1>React + Rust (Wasm) Image Processor</h1>
<p>
Wasm Status:{" "}
{isWasmLoaded ? (
<span style={{ color: "green", fontWeight: "bold" }}>Loaded!</span>
) : (
<span style={{ color: "yellow", fontWeight: "bold" }}>
Loading...
</span>
)}
</p>
<input type="file" accept="image/*" onChange={handleImageUpload} />
<div className="controls">
<button
onClick={applyGrayscaleFilter}
disabled={!isWasmLoaded || !originalImage}
className="wasm-button"
>
Apply Grayscale (Wasm)
</button>
<button
onClick={applyWatermarkFilter}
disabled={!isWasmLoaded || !originalImage}
className="wasm-button"
>
Apply Watermark (Wasm)
</button>
<button
onClick={resetImage}
disabled={!originalImage}
className="wasm-button"
>
Reset Image
</button>
</div>
<canvas
ref={canvasRef}
style={{ marginTop: "20px", maxWidth: "100%" }}
/>
</header>
</div>
);
}
export default App;
Desktop
In our previous example, we had a stack of React for the UI and Rust for the core logic, we are in an incredibly strong position from the get-go. We can leverage both of these investments directly for our desktop application.
Tauri is almost certainly the best choice for our project.
What it is: Tauri is a modern framework for building lightweight, secure, and fast desktop applications using web technologies for the frontend. Crucially, its backend is written in Rust.
Tauri over Electron: Electron has been a popular choice for JavaScript UI based desktop applications for a long time. It has a backend of Node as opposed to Rust for Tauri.
Tauri is often chosen over Electron due to its smaller bundle sizes, better performance, and increased security. While Electron uses a bundled version of Chromium, Tauri leverages the system's webview, leading to smaller application sizes and reduced memory consumption.
How it would work for your app:
Frontend: We would place the existing React application inside the Tauri "webview". Tauri uses the operating system's native web rendering engine (WebView2 on Windows, WebKit on macOS, WebKitGTK on Linux) instead of bundling a full browser like Electron.
Backend/Core Logic: Instead of compiling the Rust image processing logic to
WebAssembly
, we would compile it as a native Rust library. The frontend can then call these Rust functions directly and asynchronously through Tauri's simple JavaScript API.
Note: The following code snippets only highlight the core functionality and requirements. To enact the same, please setup a tauri project on your machine. Also, rust crates (dependencies) work with a concept of conditional compilation in the form of features in the cargo.toml file, so certain peripheral functionalities would require additional features to be declared with the dependencies. (The rust compiler will flag such cases, so you are mostly covered there.)
[package]
name = "image-dobara"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "image_dobara_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
image = "0.24"
imageproc = "0.23"
rusttype = { version = "0.9.3" }
dirs = "6.0.0"
tauri-plugin-dialog = "2"
Above is the toml
file which is present in the src-tauri
folder inside the parent react
folder.
use image::{Rgba};
use imageproc::drawing::draw_text_mut;
use rusttype::{Font, Scale};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::command;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![apply_grayscale, add_watermark])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn get_output_path(input_path: &str, suffix: &str) -> Result<String, String> {
let path = PathBuf::from(input_path);
let stem = path
.file_stem()
.ok_or("Invalid file name")?
.to_str()
.ok_or("Invalid file name")?;
let extension = path
.extension()
.ok_or("No extension")?
.to_str()
.ok_or("Invalid extension")?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| e.to_string())?
.as_millis();
let new_filename = format!("{}_{}_{}.{}", stem, suffix, timestamp, extension);
// THIS IS THE DIRS CRATE WAY:
// The `dirs::download_dir()` function returns an `Option<PathBuf>`.
// We convert the `None` case into an `Err` to fit our function's return type.
let downloads_dir =
dirs::download_dir().ok_or("Could not find the user's Downloads directory")?;
let output_path = downloads_dir.join(new_filename);
Ok(output_path
.to_str()
.ok_or("Invalid output path")?
.to_string())
}
#[command]
fn apply_grayscale(image_path: String) -> Result<String, String> {
let mut img = image::open(&image_path).map_err(|e| e.to_string())?;
img = img.grayscale();
let output_path = get_output_path(&image_path, "grayscale")?;
img.save(&output_path).map_err(|e| e.to_string())?;
Ok(output_path)
}
#[command]
fn add_watermark(image_path: String, watermark_text: String) -> Result<String, String> {
let mut img = image::open(&image_path).map_err(|e| e.to_string())?;
// Load the font data from the bundled asset
let font_data: &[u8] = include_bytes!("../assets/fonts/Roboto-VariableFont_wdth,wght.ttf");
let font: Font<'static> = Font::try_from_bytes(font_data).ok_or("Failed to load font")?;
let height = img.height() as f32;
let scale = Scale {
x: height * 0.1,
y: height * 0.1,
};
let text_color = Rgba([255u8, 255u8, 255u8, 255u8]);
let x_pos = (img.width() as f32 * 0.1) as i32;
let y_pos = (img.height() as f32 * 0.1) as i32;
draw_text_mut(
&mut img,
text_color,
x_pos,
y_pos,
scale,
&font,
&watermark_text,
);
let output_path = get_output_path(&image_path, "watermarked")?;
// Handle image save errors with a more descriptive message
match img.save(&output_path) {
Ok(_) => Ok(output_path),
Err(e) => {
eprintln!("\n\nFailed to save watermarked image: {}\n\n", e);
return Err(format!("Failed to save watermarked image: {}", e));
}
}
// img.save(&output_path).map_err(|e| e.to_string())?;
// Ok(output_path)
}
This is the core functionality (example, which is bundled as tauri handlers and exposed in the main.rs
file)
Keep vigil about the tauri plugins and their required permission configuration.
Note: You can configure a lot of metadata around the desktop application in general via tauri.conf.json
file.
Now for the frontend application,
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
import { convertFileSrc } from "@tauri-apps/api/core";
import "./App.css";
function App() {
const [selectedImagePath, setSelectedImagePath] = useState<string | null>(
null
);
const [watermarkText, setWatermarkText] = useState<string>("aatir");
const [processedImagePath, setProcessedImagePath] = useState<string | null>(
null
);
const selectImage = async () => {
// Reset previous results
setProcessedImagePath(null);
try {
// The `open` function is now imported from the dialog plugin
const result = await open({
multiple: false,
filters: [{ name: "Images", extensions: ["png", "jpeg", "jpg"] }],
});
// In v2, the `open` function directly returns the path as a string,
// an array of strings, or null. No need to access a `.path` property.
if (typeof result === "string") {
setSelectedImagePath(result);
} else {
// User cancelled the dialog
setSelectedImagePath(null);
}
} catch (error) {
// This will catch errors if the plugin isn't configured correctly
console.error("Error selecting image:", error);
alert(`Error selecting image: ${error}`);
}
};
const applyGrayscale = async () => {
if (!selectedImagePath) {
alert("Please select an image first!");
return;
}
try {
// `invoke` from `@tauri-apps/api/core` is correct
const newPath = await invoke<string>("apply_grayscale", {
imagePath: selectedImagePath,
});
setProcessedImagePath(newPath);
} catch (error) {
alert(`Error: ${error}`);
}
};
const addWatermark = async () => {
if (!selectedImagePath) {
alert("Please select an image first!");
return;
}
try {
const newPath = await invoke<string>("add_watermark", {
imagePath: selectedImagePath,
watermarkText: watermarkText,
});
setProcessedImagePath(newPath);
} catch (error) {
alert(`Error: ${error}`);
}
};
return (
<div className="container">
<h1>Image Processor (Aatir)</h1>
<div className="row">
<button onClick={selectImage}>Select Image</button>
{selectedImagePath && (
<p className="label">
Selected: <code>{selectedImagePath.split(/[/\\]/).pop()}</code>
</p>
)}
</div>
<div className="row">
<label htmlFor="watermark">Watermark Text:</label>
<input
id="watermark"
onChange={(e) => setWatermarkText(e.currentTarget.value)}
value={watermarkText}
placeholder="Enter watermark text..."
disabled={!selectedImagePath}
/>
</div>
<div className="row">
<button onClick={applyGrayscale} disabled={!selectedImagePath}>
Apply Grayscale
</button>
<button onClick={addWatermark} disabled={!selectedImagePath}>
Add Watermark
</button>
</div>
{processedImagePath && (
<div className="result-container">
<h2>Result</h2>
<p>
Saved at: <code>{processedImagePath}</code>
</p>
{/* <img
src={processedImagePath}
alt="Processed"
className="result-image"
/> */}
</div>
)}
</div>
);
}
export default App;
As you can see, in the snippet above, the tauri designated handlers are invoked where the function names are provided as string along with its potential parameters.
How do you think that works??
The general direction we want to take, is that, all the commands are loaded into the application context in the form of a HashMap (approx.)
; Tauri injects some Inter Process Communication (IPC) implementation in the webView
somehow; I create a Promise
in JS for invoking the function, that function name is matched; it is called, executed and it returns some value (json serialization and deserialization
is happening where it may), which is recieved as Resolve
by the JS interface. If your thoughts were aligned with this claim, then bravo!!
We will not indulge into further details pertaining the internal workings of tauri, as it is out of the context of this article, but the main star of the show is IPC (it is all about the transit of request and response from one point to another). Since, the UI application and the rust backend run on the same process, this message passing has extremely low latency.
Partition
There is another segment to this article. You can find it here:
I realize article is long, but please take your time with it. You don’t even have to go through the entire thing in a single sitting. What I do hope for, is that you appreciate the read, and actively take in what you go through here. It would not do, to just glance through it with the insight that I just wrote this for the sake of writing it. Thank you.
Subscribe to my newsletter
Read articles from Aatir Nadim directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
