Skip to content

rrmolr8-ctrl/project-lotus

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

76 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸͺ· Lotus (lotus-gui)

Version 0.3.0 (Current)

  • Status: Beta? I'm not sure what to call it, but it works way better than any alpha ive seen! but hasnt been around long enough to call it stable yet. This is a big release and brings with it support for linux and windows installers for apps built with lotus. This allows you to build apps with lotus and just npx lotus build and it will build the app installer for you for your platform. It is still a bit early so any feedback is appreciated! I plan for this to be a continued project for a while and I'm excited to see what we can do with it.

Supported-Installers

  • Windows: MSI, EXE
  • Linux: RPM, DEB, AppImage, Pacman, Flatpak
  • Mac: None (yet! please feel free to contribute!)

Supported-Runtime-Platforms

  • Windows: 10, 11
  • Linux: Arch, Debian, Fedora, openSUSE, Ubuntu
  • Mac: None (yet! please feel free to contribute!)

In general linux, bsd, and windows are supported. Mac is not supported because I don't have a mac. I dont know enough about bsd or suse to automate building installers for them yet but it should fully run there and if not please feel free to open an issue and i will do my best to resolve it!

πŸ† "ELECTRON IS AN 80s FORD BRONCO." "Huge. Heavy. Built to survive off-roading, river crossings, and the open internet. Every window spins up a full browser like it's about to get lost in the wilderness."

🏎️ "LOTUS IS... WELL, A LOTUS ELISE." "If a part doesn't make it start faster, use less memory, or render pixels, it's gone. No extra suspension. No spare tires. No browser pretending to be an operating system."

πŸ₯Š THE ARCHITECTURE (Or: Why It's Fast) "Most desktop apps are just opening a preferences panel. I didn't think that required a second operating system."

β€’ Electron Strategy: Puts the browser in charge and lets Node ride shotgun. "It builds a monster truck because it assumes you're off-roading." β€’ Lotus Strategy: The opposite. "Node owns the OS. Servo paints the pixels. No magic. No fake sandboxes. No hidden Chromium instances listening to how you use each piece of chrome for telemetry."

🚨 STATUS: ALPHA (BUT IT WORKS) We have working Windows and Linux builds available on npm (@lotus-gui/core@0.2.0). Mac support is missing (because their ecosystem needs an adult, please be its adult!). BSD and SUSE support is planned (because I know the pain points over there, see Roadmap). Tested support for building fully packaged .rpm installers for Linux but should support .deb and basic windows installers.

πŸ”§ THE ANALOGY THAT EXPLAINS EVERYTHING: β€’ Node.js is the track. β€’ Servo is the car. β€’ IPC is the steering wheel. "On a track, you don't worry about potholes. You worry about lap times."

TL;DR: Electron assumes you're lost. Lotus assumes you know where you're going. And that's why it's fast.


πŸ’‘ THE POINT: "Node.js already does OS integration. We just needed a renderer. That's it. That's the whole project."

πŸš€ Features (The Good Stuff)

  • Speed that actually matters:

    • Cold start to interactive window in <300ms. You can't even blink that fast.
    • A single window stack (Rust + Node + Servo) runs on ~300MB RAM.
    • Adding a second window costs ~80MB. We share the renderer. We don't spawn a new universe for every pop-up.
  • Hybrid Runtime:

    • Core: Rust-based Servo engine. It renders HTML/CSS. That's it.
    • Controller: Node.js main thread. It does literally everything else.
  • Hybrid Mode (File Serving):

    • Custom Protocol: lotus-resource:// serves files from disk.
    • Why? Because spinning up an HTTP server just to show a JPEG is stupid.
    • Security: Directory jailing. You can't ../../ your way to /etc/passwd. Nice try.
  • Advanced IPC (The Steering Wheel):

    • WebSocket IPC Server: We use tokio + axum on 127.0.0.1:0 with persistent WebSocket connections. It works. It's pretty dam fast and low latency.
    • Auto-Adapting: JSON? Binary? Blobs? We don't care. We handle it via WebSockets natively.
    • MsgPack Batching (Pipelines): We pack small messages together and unleash them in bursts to avoid starving the Winit rendering thread.
    • Zero-Copy Routing: We avoid parsing giant megabytes of JSON simply to route a window event. Copying/allocating data is for people who like waiting.
    • invoke() / handle() (Promise IPC): The renderer calls window.lotus.invoke('ch', data) and gets back a real Promise. Node registers ipcMain.handle('ch', async fn). No manual reply channels, no leaked listeners, no correlation IDs in your app code.
    • File Drag-and-Drop: OS-level file drag is intercepted from winit and forwarded to both the renderer (window.lotus.on('file-drop', ...)) and Node.js (win.on('file-drop', ...)). Zero Servo involvement -- pure winit event forwarding.
  • Window State Persistence:

    • It remembers where you put the window (if you give it an ID). Groundbreaking technology, I know.
    • Handles maximized state, size, position. You're welcome.
  • Script Injection:

    • Execute arbitrary JS in the renderer from the main process. God mode unlocked.
  • Native Look & Feel:

    • true OS transparency, and actual working cursors. We don't just emulate a window; we are a window.
    • No White Flash: We paint transparently. Your users won't be blinded by a white box while your 5MB of JS loads.
  • Frameless Windows:

    • Kill the title bar. Remove the frame. Build whatever crap you want.
    • Custom Drag Regions: Mark any element with -webkit-app-region: drag or data-lotus-drag. Lotus bridges it to the OS -- no JS required.
    • Custom Resize Borders: 8px invisible resize handles on every edge and corner. They just work.
    • Cursor-Aware: Resize cursors show up at the borders. Servo drives all other cursors (grab, pointer, text, etc.) -- no interference.
  • Multi-Window Support:

    • Spawn multiple independent windows from a single Node process.
    • Shared renderer = ~80MB per extra window.

πŸ“¦ Monorepo Structure

Lotus is organized as a monorepo with two packages:

lotus/
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ lotus-core/          # @lotus-gui/core -- Runtime engine (Servo + Node bindings)
β”‚   β”‚   β”œβ”€β”€ src/             # Rust source (N-API bindings, window management)
β”‚   β”‚   β”œβ”€β”€ lotus.js         # High-level JS API (ServoWindow, IpcMain, App)
β”‚   β”‚   β”œβ”€β”€ index.js         # Native binding loader
β”‚   β”‚   β”œβ”€β”€ resources/       # IPC bridge scripts, debugger
β”‚   β”‚   └── test_app/        # Example application
β”‚   β”‚
β”‚   └── lotus-dev/           # @lotus-gui/dev -- CLI toolkit for development & packaging
β”‚       β”œβ”€β”€ bin/lotus.js      # CLI entry point (lotus dev, build, clean)
β”‚       └── lib/templates/    # Installer templates (RPM spec, etc.)
β”‚
β”œβ”€β”€ package.json             # Monorepo root (npm workspaces)
└── README.md                # You are here
Package npm Name What It Does
lotus-core @lotus-gui/core The runtime -- Servo engine, window management, IPC. This is what your app require()s.
lotus-dev @lotus-gui/dev CLI toolkit -- dev server with hot-reload, build system, DEB/RPM installer packaging.

πŸ› οΈ Prerequisites

If you want to run this, you need to be on an OS that respects you. (to be fair the plan is to support all platforms, so this is more of a joke to clarify)

Linux (Debian/Ubuntu/Fedora)

This is where development happens. It works here. Fully working .node file for Linux is in the artifacts tab.

  • Node.js: v22+. Don't come at me with v14, we legit require it, we are using N-API 4.

  • System Libraries: You need these or things will scream at you.

    Ubuntu/Debian:

    sudo apt-get update
    sudo apt-get install libgl1-mesa-dev libssl-dev python3 libfontconfig1-dev
    
    # Required for building .deb installers with `lotus build`
    sudo apt-get install dpkg-dev fakeroot

    Fedora:

    sudo dnf install mesa-libGL-devel openssl-devel python3 fontconfig-devel
    
    # Required for building .rpm installers with `lotus build`
    sudo dnf install rpm-build

Note: We auto-fix the GLIBC_TUNABLES static TLS issue. If you see ERR_DLOPEN_FAILED and the app restarts itself, that's just Lotus fixing your environment for you. Don't panic.

Windows

  • Status: Alpha ("It Works!")
  • NPM: βœ… Verified - @lotus-gui/core includes the pre-built Windows binary.
  • Build Requirements (only if building from source): Visual Studio Build Tools + choco install llvm nasm python311.

macOS

  • Status: HELP WANTED πŸ†˜
  • I removed CI support because I honestly just don't know enough about the Mac app lifecycle to do it right. If you are a Mac developer and want to fix this, PRs are welcome. I just don't have a system to test on. "Here be dragons still." πŸ‰ (Translation: Please save me from Xcode.)

πŸ›  Platform Support Matrix

Platform Arch Native Binary (.node) Installer Target Status
Linux (Debian/Ubuntu) x64 βœ… Verified .deb (Stable) Ready
Linux (Fedora/RHEL) x64 βœ… Verified .rpm (Stable) Ready
Linux (openSUSE) x64 πŸ›  Testing Planned Alpha
Windows x64 βœ… Verified .msi (testing) Beta *1
FreeBSD x64 πŸ›  Testing Planned Alpha
macOS arm64 πŸ†˜ Help Wanted TBD On Hold

*1 Windows Beta Status:

  • What Works:

    • βœ… Native Binary: Pre-built .node available on npm
    • βœ… Build System: lotus build creates .msi installers
    • βœ… Runtime: Angle + Servo + Node.js run correctly
    • βœ… VC++ Redist: Auto-embedded and installed silently
    • βœ… Icon Handling: PNG/JPG β†’ ICO conversion for installers
    • βœ… PE Header Patching: No black console window on launch
  • Known Issues:

    • ⚠️ Installer Signing: Not yet implemented (requires EV cert)
    • ⚠️ NSIS Fallback: lotus build --target nsis still uses old logic
    • ⚠️ Drag-and-Drop: Not yet tested on Windows

Note:

  • Installer Target: The packaged distribution format (what users download/install)
  • Native Binary: The .node file that powers the runtime (what developers require())

Verification Progress Tracking

For a detailed breakdown of build success, environmental linking, and functional testing per platform, see the master tracking issue: πŸš€ Multi-Platform Support & Build Targets.

  • Build Success: CI/CD produces artifacts without warnings.
  • Functionality: Core features operational on clean installs.
  • Environment: Proper linking of native dependencies.

πŸš€ Quick Start (Usage from NPM)

The Easy Way: working Windows and Linux builds are on npm. You don't need to build from source.

Option 1: Quick Start (Recommended)

The fastest way to get started is with the CLI:

npx lotus init my-app
cd my-app
npm install
npx lotus dev

Option 2: Manual Setup

If you prefer to set things up yourself:

mkdir my-lotus-app && cd my-lotus-app
npm init -y
npm install @lotus-gui/core @lotus-gui/dev

Then see the example code below or check node_modules/@lotus-gui/core/test_app for a full reference.

Step 1: Create lotus.config.json

This file controls your app's metadata and build settings:

{
    "name": "MyApp",
    "version": "1.0.0",
    "license": "MIT",
    "description": "My desktop app, minus the bloat",
    "main": "main.js",
    "executableName": "my-app",
    "icon": "./assets/icon.png",
    "build": {
        "linux": {
            "wmClass": "my-app",
            "categories": ["Utility"]
        }
    }
}

Step 2: Create main.js

const { ServoWindow, ipcMain, app } = require('@lotus-gui/core');
const path = require('path');

app.warmup(); // Wake up the engine

const win = new ServoWindow({
    id: 'main-window',
    root: path.join(__dirname, 'ui'),
    index: 'index.html',
    width: 1024,
    height: 768,
    title: "My Lotus App",
    transparent: true,
    visible: false
});

// Show only after first frame -- no white flash, ever
win.once('frame-ready', () => win.show());

// IPC: talk to the webpage
ipcMain.on('hello', (data) => {
    console.log('Renderer says:', data);
    ipcMain.send('reply', { message: 'Hello from Node!' });
});

// Or use invoke() for a clean request/reply pattern
ipcMain.handle('get-time', async () => {
    return { ts: Date.now() };
});

Step 3: Create your UI

mkdir ui

ui/index.html:

<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body style="background: transparent;">
    <div style="background: rgba(0,0,0,0.9); color: white; padding: 2rem; border-radius: 12px;">
        <h1>Hello from Lotus! πŸͺ·</h1>
        <button onclick="window.lotus.send('hello', { from: 'renderer' })">
            Talk to Node.js
        </button>
        <button onclick="getTime()">
            Invoke get-time (Promise)
        </button>
    </div>
    <script>
        window.lotus.on('reply', (data) => {
            console.log('Node says:', data.message);
        });
        async function getTime() {
            const { ts } = await window.lotus.invoke('get-time');
            console.log('Server time:', new Date(ts).toISOString());
        }
    </script>
</body>
</html>

Step 4: Run it

npx lotus dev main.js

βš™οΈ lotus.config.json Reference

The config file lives in your project root and controls both runtime behavior and build output.

Field Type Required Description
name string Yes Application display name
version string Yes Semver version (e.g., "1.0.0")
license string No SPDX license identifier. Defaults to "Proprietary"
description string No Short description (used in package managers)
main string No Entry point file. Falls back to package.json main, then index.js
executableName string No Binary name (e.g., my-app β†’ /usr/bin/my-app). Defaults to lowercase name
icon string No Path to app icon (relative to project root)
author string No Maintainer name for package metadata
homepage string No Project URL
build.linux.wmClass string No Window manager class (taskbar grouping)
build.linux.section string No Package section (default: "utils")
build.linux.categories string[] No Desktop entry categories

πŸ”§ CLI Commands (@lotus-gui/dev)

The @lotus-gui/dev package provides the lotus CLI:

# Start dev server with hot-reload (watches for changes, auto-restarts)
lotus dev [entry]

# Build a distributable installer (DEB or RPM)
lotus build --platform <linux|win32> --target <deb|rpm>

# Clean build artifacts (removes dist/)
lotus clean

See the full @lotus-gui/dev documentation for details on build output, flags, and project setup.

🎯 Usage (Code Snippets)

Hybrid Mode: Serving Files

Stop using Express to serve static files. It's embarrassing.

const { ServoWindow, app } = require('@lotus-gui/core');

app.warmup(); // Wake up the engine

const win = new ServoWindow({
    root: '/absolute/path/to/ui',  // Jail the renderer here
    index: 'index.html',            // Start here
    width: 1024,
    height: 768,
    title: "My Hybrid Lotus App"
});

// Now serving at lotus-resource://localhost/index.html

IPC: Talking to the Machine

The renderer is a webpage. The main process is Node. They talk.

Renderer (The Webpage):

// Fire and forget.
window.lotus.send('channel', { magic: true });

// Binary.
const blob = new Blob(['pure binary fury']);
window.lotus.send('binary-channel', blob);

// Promise-based request/reply -- no manual reply channels needed.
const result = await window.lotus.invoke('get-data', { id: 42 });
console.log(result);

Main Process (Node):

const { ipcMain } = require('@lotus-gui/core');

// Fire-and-forget listener.
ipcMain.on('channel', (data) => {
    console.log('Renderer said:', data);
    ipcMain.send('reply', { status: 'acknowledged' });
});

// Request/reply handler -- pairs with window.lotus.invoke().
ipcMain.handle('get-data', async ({ id }) => {
    return await db.find(id); // returned value auto-sent to renderer
});

Native Transparency: "Ghost Mode"

Want a window that keeps the OS vibe? We bridge OS transparency directly to your CSS.

const win = new ServoWindow({
    transparent: true, // The magic switch
    title: "Ghost Window"
});

How it works:

  1. We set the Servo shell background to 0x00000000 (fully transparent).
  2. We tell the OS to make the window transparent.
  3. Result: The window is invisible. The only thing visible is what you paint.

In your CSS:

/* This makes the whole app see-through to the desktop */
body {
    background: transparent; 
}

/* This adds a semi-transparent glass effect */
.container {
    background: rgba(0, 0, 0, 0.8); 
    color: white;
}

The "White Flash" Killer: Because the default backbone is transparent, there is zero white flash on startup. If your app takes 10ms to load, the user sees their wallpaper for 10ms, not a blinding white rectangle. You're welcome.

Frameless Windows: "Build Your Own Window"

Tired of the OS telling you what your title bar looks like? Remove it.

const win = new ServoWindow({
    frameless: true,         // Kill the native frame
    transparent: true,       // Optional: go fully borderless
    title: "My Borderless App"
});

Out of the box you get:

  • 8px resize borders on every edge/corner -- just move the mouse to the edge.
  • Drag regions driven by CSS -- no JS wiring required.

In your HTML:

<!-- These two approaches both work -->
<div style="-webkit-app-region: drag; cursor: grab;">Drag me to move the window</div>
<div data-lotus-drag="true">Also works</div>

Lotus auto-detects elements with -webkit-app-region: drag or data-lotus-drag via injected JS and sends their coordinates to Rust. Mouse down on one of those elements β†’ drag_window(). Mouse down on the border β†’ drag_resize_window(). Everything else β†’ Servo handles it normally.

To exclude an element inside a drag region (like a close button), use -webkit-app-region: no-drag or data-lotus-drag="false".

Window Controls & Lifecycle

Full control over the OS window manager directly from JavaScript. You don't need to write native code to build a custom title bar.

// Window manipulation
win.minimize();
win.unminimize();
win.maximize();
win.unmaximize();
win.focus();
win.setMinSize(800, 600); // enforce a minimum, or pass 0,0 to clear
win.setMaxSize(1920, 1080);

// Listen to OS-level events
win.on('moved', ({ x, y }) => console.log('Window moved to', x, y));
win.on('resize', ({ width, height }) => console.log('Resized to', width, height));
win.on('focus', () => console.log('Window gained focus'));
win.on('blur', () => console.log('Window lost focus'));

// File drag-and-drop (OS-level, works everywhere)
win.on('file-hover', ({ path }) => console.log('Hovering:', path));
win.on('file-hover-cancelled', () => console.log('Drag cancelled'));
win.on('file-drop', ({ path }) => console.log('Dropped:', path));

// Renderer-side (same events arrive via window.lotus.on)
// window.lotus.on('file-drop', ({ path }) => showDropOverlay(path));

Multi-Window Support

Creating specific windows? Easy. They share the same renderer instance, so it costs ~80MB per extra window instead of ~300MB.

const win1 = new ServoWindow({ title: "Window 1" });
const win2 = new ServoWindow({ title: "Window 2" });
const win3 = new ServoWindow({ title: "Window 3" });
// All three windows share the same renderer process.
// Efficient.

Window State Persistence: "Total Recall"

By default, windows have the memory span of a goldfish. They forget where they were. If you want them to remember, give them a name.

const win = new ServoWindow({
    id: "main-window", // REQUIRED for state saving
    title: "I Remember Everything",
    restoreState: true // Default is true, obviously
});

The Logic:

  • No ID? We generate a random UUID. New session, new window, default size.
  • With ID? We check ~/.config/app-name/window-state.json. If we've seen "main-window" before, we put it back exactly where you left it.
  • It snaps back to the last known position faster than you can say "Electron is bloat."

Building Distributable Packages

When you're ready to ship your application, Lotus uses a highly optimized Node Single Executable Application (SEA) generation pipeline tied directly into CrabNebula to spit out installer payloads for every OS.

  1. Ensure your lotus.config.json is properly configured with your app name, author, icon paths, and resources arrays for extra assets.

  2. Run the build command with your target OS installer format:

    # Linux Formats
    npx lotus build --target appimage
    npx lotus build --target deb
    npx lotus build --target pacman
    
    # Windows Formats
    npx lotus build --target exe
    npx lotus build --target msi

Because Lotus relies on native .node bindings, you must build for the OS you are currently on (e.g., execute --target exe inside a Windows CI/CD runner to get Windows binaries).

Your app is now a real installed application with a binary in /usr/bin/ and everything. Just like a grown-up program.


πŸ—οΈ Building from Source (The Waiting Game)

Pro Tip: You don't actually have to build this yourself. Check the Actions tab on GitHub. Every commit produces working artifacts for Linux and Windows. Download, unzip, use the time saved to beat that level you've been procrastinating on. (expect npm install support without having to build yourself soon -- you can just grab the .node files from the artifacts tab)

git clone https://github.com/1jamie/project-lotus.git
cd project-lotus
npm install

Build the Native Addon:

cd packages/lotus-core

# Debug Build (Faster compilation, still slow)
npm run build:debug

# Release Build (Optimized, takes eons)
npm run build

Additional Requirements for Building:

  • Rust: Stable toolchain.
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • Windows: Visual Studio Build Tools + choco install llvm nasm python311

Warning: The first build takes forever. You are compiling a browser engine and a Node runtime binding. Go make a coffee. Read a book. Learn a new language. (though we all know you are scrolling TikTok or Reddit, we all know you aren't being productive while the compile runs, none of us ever are) It gets faster after the first time. I promise.

πŸ“‚ Project Structure (For the curious)

  • packages/lotus-core/src/lib.rs - The Brain. Main Rust entry point. Handles N-API, Event Loop, IPC.
  • packages/lotus-core/src/window_state.rs - The Memory. Remembers where you put your windows.
  • packages/lotus-core/src/platform.rs - The Politeness. Proper OS integrations.
  • packages/lotus-core/lotus.js - The Body. High-level Node.js wrapper (ServoWindow, IpcMain, App).
  • packages/lotus-core/index.js - The Glue. Native binding loader.
  • packages/lotus-core/test_app/ - The Real Demo. Full-featured test app.
  • packages/lotus-dev/bin/lotus.js - The Toolbox. CLI for dev, build, and clean commands.
  • packages/lotus-dev/lib/templates/ - The Factory. Installer templates (RPM spec, etc.).

For detailed API documentation, see:

🀝 Contributing

PRs are welcome. If you break the winit or glutin version requirements, I will close your PR with extreme prejudice. We need specific embedding traits and I'm already sitting on the edge with winit 0.30.2, don't push me off the edge it has already mentally put me on!

  1. Fork it.
  2. Branch it (git checkout -b feature/cool-stuff).
  3. Commit it (git commit -m 'Added cool stuff').
  4. Push it.
  5. PR it.

πŸ—ΊοΈ Roadmap

v0.2.0: The Shell & Frame (RELEASED)

  • βœ… Frameless Mode: Toggle window decorations off.
  • βœ… CSS Dragging: Bridge for custom CSS drag areas (-webkit-app-region: drag, data-lotus-drag).
  • βœ… Resize Borders: Custom 8px resize hit-zones on all edges/corners.
  • βœ… Dev CLI: lotus init command added to create a new Lotus project.

Note: Surprise! I changed my mind and gave you frameless support lol. I was just gonna say deal with it and give you native menu support but then i realized the headache.... so here we are! surprise! Now you can do wtf you want with window decorations!

v0.3.0: The Support Expansion

  • Windows Support: Full MSI/EXE distribution (moving beyond just the .node binary).
  • BSD Support: Bringing the renderer to the BSD community.
  • SUSE Support: Expanding @lotus-gui/dev to handle OpenSUSE RPM quirks.
  • Mac Support? (If someone donates a Mac or a contributor steps up).
  • Verify/support build for all supported platforms.: Go through the build process for all supported platforms and verify that it works and fix where it does not.

v0.4.0: The Future (Community Input Welcome)

  • Build optimization: See what we can do about the electron builder as it does add deps we may not need and may be able to make it install less deps during the install processes.
  • Debugger: Add a debugger to the dev CLI to give access to a dev console for helping with development. Will probably have a seperate package called lotus-core-debug that the dev CLI will use to start the app with a build of lotus with the debug symbols and a developers console in its own window to help with debugging and gui development.
  • Open to suggestions: I'm open to suggestions for the future. If you have an idea, let me know. Right now v0.4.0 is just a rough tenative plan for what I might do in the future.

License: MIT. Do whatever you want, just don't blame me if your computer achieves sentience and takes flight.

P.S.

The entire framework core is ~2,500 lines of code.

If that feels suspiciously light, it's because it is. I didn't try to build an OS inside your OS; I just gave Node a window and cut the fat until there was nothing left but speed.

Electron carries the weight of the world. Lotus just carries the pixels.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Rust 45.4%
  • JavaScript 35.5%
  • HTML 18.1%
  • Other 1.0%