feat: start repository

This commit is contained in:
Gustavo Carvalho
2025-12-11 01:58:08 -03:00
commit f4c3ca0a76
57 changed files with 12888 additions and 0 deletions

179
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,179 @@
# Contributing to Win11 Clipboard History
First off, thank you for considering contributing to Win11 Clipboard History! 🎉
## 📋 Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Making Changes](#making-changes)
- [Pull Request Process](#pull-request-process)
- [Style Guidelines](#style-guidelines)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Features](#suggesting-features)
## Code of Conduct
This project and everyone participating in it is governed by our commitment to creating a welcoming and inclusive environment. Please be respectful and constructive in all interactions.
## Getting Started
1. **Fork the repository** on GitHub
2. **Clone your fork** locally:
```bash
git clone https://github.com/YOUR-USERNAME/win11-clipboard-history-for-linux.git
cd win11-clipboard-history-for-linux
```
3. **Add the upstream remote**:
```bash
git remote add upstream https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux.git
```
## Development Setup
### Prerequisites
Make sure you have the required dependencies installed:
```bash
# Install system dependencies (auto-detects your distro)
make deps
# Install Rust and Node.js if needed
make rust
make node
source ~/.cargo/env
# Verify everything is installed
make check-deps
```
### Running in Development Mode
```bash
# Install npm dependencies
npm install
# Start development server with hot reload
make dev
```
## Making Changes
1. **Create a new branch** from `master`:
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix
```
2. **Make your changes** and test them locally
3. **Run linters and formatters**:
```bash
make lint
make format
```
4. **Commit your changes** with a descriptive message:
```bash
git commit -m "feat: add amazing new feature"
# or
git commit -m "fix: resolve clipboard paste issue on Wayland"
```
### Commit Message Convention
We follow [Conventional Commits](https://www.conventionalcommits.org/):
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation changes
- `style:` - Code style changes (formatting, etc.)
- `refactor:` - Code refactoring
- `perf:` - Performance improvements
- `test:` - Adding or updating tests
- `chore:` - Maintenance tasks
## Pull Request Process
1. **Update your branch** with the latest upstream changes:
```bash
git fetch upstream
git rebase upstream/master
```
2. **Push your branch** to your fork:
```bash
git push origin feature/your-feature-name
```
3. **Create a Pull Request** on GitHub
4. **Fill out the PR template** completely
5. **Wait for review** - maintainers will review your PR and may request changes
### PR Requirements
- [ ] Code follows the project's style guidelines
- [ ] All linters pass (`make lint`)
- [ ] Changes are tested locally
- [ ] Documentation is updated if needed
- [ ] PR description clearly explains the changes
## Style Guidelines
### TypeScript/React
- Use functional components with hooks
- Follow existing component patterns
- Use TypeScript types (avoid `any`)
- Use meaningful variable and function names
### Rust
- Follow Rust idioms and best practices
- Use `cargo fmt` for formatting
- Address all `clippy` warnings
- Document public functions and modules
### CSS/Tailwind
- Use Tailwind utility classes
- Follow the Windows 11 design system defined in `tailwind.config.js`
- Support both light and dark modes
## Reporting Bugs
Before reporting a bug:
1. **Search existing issues** to avoid duplicates
2. **Try the latest version** - the bug might be fixed
3. **Collect information**:
- OS and version
- Desktop environment
- Display server (X11/Wayland)
- Steps to reproduce
- Error messages/logs
Then [create a bug report](https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux/issues/new?template=bug_report.md).
## Suggesting Features
We welcome feature suggestions! Before suggesting:
1. **Check if it aligns** with the project's goal (Windows 11-style clipboard manager)
2. **Search existing issues** to avoid duplicates
3. **Consider implementation** - how complex would it be?
Then [create a feature request](https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux/issues/new?template=feature_request.md).
## Questions?
Feel free to [open a discussion](https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux/discussions) for questions or ideas.
---
Thank you for contributing! 🙏

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# Funding
If you find Win11 Clipboard History useful, consider supporting its development:
github: [gustavosett]

53
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,53 @@
---
name: Bug Report
about: Report a bug or issue with Win11 Clipboard History
title: '[BUG] '
labels: ['bug', 'triage']
assignees: ''
---
## 🐛 Bug Description
A clear and concise description of what the bug is.
## 📋 Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Press '...'
4. See error
## ✅ Expected Behavior
A clear and concise description of what you expected to happen.
## ❌ Actual Behavior
What actually happened instead.
## 📸 Screenshots
If applicable, add screenshots to help explain your problem.
## 🖥️ Environment
- **OS**: [e.g., Ubuntu 22.04, Fedora 39, Arch Linux]
- **Desktop Environment**: [e.g., GNOME 45, KDE Plasma 5.27]
- **Display Server**: [X11 / Wayland]
- **App Version**: [e.g., 1.0.0]
- **Installation Method**: [DEB, RPM, AppImage, Built from source]
## 📝 Additional Context
Add any other context about the problem here.
## 📄 Logs
<details>
<summary>Click to expand logs</summary>
```
Paste any relevant logs here
```
</details>

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 📖 Documentation
url: https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux#readme
about: Read the README for installation and usage instructions
- name: 💬 Discussions
url: https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux/discussions
about: Ask questions and share ideas in GitHub Discussions

View File

@@ -0,0 +1,37 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: '[FEATURE] '
labels: ['enhancement']
assignees: ''
---
## 🚀 Feature Description
A clear and concise description of the feature you'd like to see.
## 💡 Motivation
Why would this feature be useful? What problem does it solve?
## 📝 Proposed Solution
Describe how you envision this feature working.
## 🔄 Alternatives Considered
Have you considered any alternative solutions or workarounds?
## 📸 Mockups / Examples
If applicable, add mockups, screenshots, or examples from other applications.
## 📋 Additional Context
Add any other context or information about the feature request here.
## ✅ Checklist
- [ ] I have searched existing issues to make sure this isn't a duplicate
- [ ] I have considered if this aligns with the project's goals (Windows 11-style clipboard manager)
- [ ] I am willing to help implement this feature (optional)

25
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,25 @@
---
name: Question / Help
about: Ask a question or get help with installation/usage
title: '[QUESTION] '
labels: ['question']
assignees: ''
---
## ❓ Question
What would you like to know or get help with?
## 🖥️ Environment (if applicable)
- **OS**: [e.g., Ubuntu 22.04, Fedora 39, Arch Linux]
- **Desktop Environment**: [e.g., GNOME 45, KDE Plasma 5.27]
- **Display Server**: [X11 / Wayland]
## 📝 What I've Tried
Describe what you've already attempted or researched.
## 📄 Additional Context
Add any other context, screenshots, or information here.

46
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,46 @@
## 📝 Description
<!-- Describe your changes in detail -->
## 🔗 Related Issue
<!-- Link to the issue this PR addresses (if any) -->
Fixes #
## 🧪 Type of Change
<!-- Mark the appropriate option with an 'x' -->
- [ ] 🐛 Bug fix (non-breaking change that fixes an issue)
- [ ] ✨ New feature (non-breaking change that adds functionality)
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 📚 Documentation update
- [ ] 🎨 Style/UI change
- [ ] ♻️ Refactoring (no functional changes)
- [ ] ⚡ Performance improvement
- [ ] 🧹 Chore (build process, dependencies, etc.)
## 📸 Screenshots (if applicable)
<!-- Add screenshots to show UI changes -->
## ✅ Checklist
<!-- Mark completed items with an 'x' -->
- [ ] My code follows the project's code style
- [ ] I have run `make lint` and `make format`
- [ ] I have tested my changes locally
- [ ] I have added/updated documentation as needed
- [ ] My changes don't introduce new warnings
- [ ] I have tested on both X11 and Wayland (if applicable)
## 🖥️ Testing Environment
- **OS**:
- **Desktop Environment**:
- **Display Server**: [X11 / Wayland]
## 📋 Additional Notes
<!-- Add any additional information for reviewers -->

81
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,81 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
| < 1.0 | :x: |
## Reporting a Vulnerability
We take security seriously. If you discover a security vulnerability, please follow these steps:
### 🔒 Private Disclosure
**Do NOT open a public issue for security vulnerabilities.**
Instead, please report security issues via one of these methods:
1. **GitHub Security Advisory**: Use [GitHub's private vulnerability reporting](https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux/security/advisories/new)
2. **Email**: gustaavoribeeiro@hotmail.com
### 📝 What to Include
When reporting a vulnerability, please include:
- **Description** of the vulnerability
- **Steps to reproduce** the issue
- **Potential impact** of the vulnerability
- **Suggested fix** (if you have one)
- **Your contact information** for follow-up questions
### ⏱️ Response Timeline
- **Initial Response**: Within 48 hours
- **Status Update**: Within 1 week
- **Fix Timeline**: Depends on severity
- Critical: 24-72 hours
- High: 1 week
- Medium: 2 weeks
- Low: Next release
### 🎁 Recognition
We appreciate security researchers who help keep our project safe. With your permission, we will:
- Acknowledge your contribution in release notes
- Add you to our security hall of fame (if created in the future)
## Security Best Practices
When using Win11 Clipboard History:
1. **Keep Updated**: Always use the latest version
2. **Build from Source**: When possible, verify the source code
3. **Check Signatures**: Verify release artifacts when available
4. **Report Issues**: Help us by reporting any suspicious behavior
## Known Security Considerations
### Clipboard Data
- Clipboard history is stored **locally only**
- No data is transmitted over the network
- History is stored in memory (not persisted to disk by default)
- Sensitive data copied to clipboard will be stored in history
### Permissions
- **Global hotkey capture**: Required for Super+V functionality
- **System tray**: For background operation
- **Clipboard access**: Core functionality
### Wayland Security
On Wayland, clipboard access follows the compositor's security model, which may restrict access to clipboard contents from background applications in some configurations.
---
Thank you for helping keep Win11 Clipboard History secure! 🔐

58
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
version: 2
updates:
# Maintain npm dependencies
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "npm"
commit-message:
prefix: "chore(deps):"
groups:
dev-dependencies:
dependency-type: "development"
patterns:
- "*"
production-dependencies:
dependency-type: "production"
patterns:
- "*"
# Maintain Cargo dependencies
- package-ecosystem: "cargo"
directory: "/src-tauri"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "rust"
commit-message:
prefix: "chore(deps):"
groups:
tauri:
patterns:
- "tauri*"
all-cargo:
patterns:
- "*"
exclude-patterns:
- "tauri*"
# Maintain GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 3
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "chore(ci):"

146
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,146 @@
name: CI
on:
push:
branches: [master, main, develop]
pull_request:
branches: [master, main]
env:
CARGO_TERM_COLOR: always
jobs:
# Lint and format check
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Rust
uses: dtolnay/rust-action@stable
with:
components: rustfmt, clippy
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
src-tauri/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file \
libssl-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev \
libgtk-3-dev libglib2.0-dev
- name: Install npm dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Check TypeScript
run: npm run build
- name: Check Rust formatting
run: cd src-tauri && cargo fmt --all -- --check
- name: Run Clippy
run: cd src-tauri && cargo clippy -- -D warnings
# Build for Linux
build-linux:
name: Build Linux
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Rust
uses: dtolnay/rust-action@stable
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
src-tauri/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file \
libssl-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev \
libgtk-3-dev libglib2.0-dev
- name: Install npm dependencies
run: npm ci
- name: Build Tauri app
run: npm run tauri:build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-artifacts
path: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/rpm/*.rpm
if-no-files-found: warn
# Security audit
security:
name: Security Audit
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-action@stable
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Run security audit
run: cd src-tauri && cargo audit
continue-on-error: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install npm dependencies
run: npm ci
- name: Run npm audit
run: npm audit --audit-level=high
continue-on-error: true

128
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,128 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
create-release:
name: Create Release
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create-release.outputs.id }}
upload_url: ${{ steps.create-release.outputs.upload_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Get version from tag
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Create Release
id: create-release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
body: |
## Win11 Clipboard History ${{ github.ref_name }}
A Windows 11-style Clipboard History Manager for Linux.
### Installation
#### Ubuntu/Debian
Download the `.deb` file and install:
```bash
sudo dpkg -i win11-clipboard-history_${{ steps.get_version.outputs.VERSION }}_amd64.deb
```
#### Fedora/RHEL
Download the `.rpm` file and install:
```bash
sudo rpm -i win11-clipboard-history-${{ steps.get_version.outputs.VERSION }}-1.x86_64.rpm
```
#### Any Linux (AppImage)
Download the `.AppImage` file, make it executable, and run:
```bash
chmod +x win11-clipboard-history_${{ steps.get_version.outputs.VERSION }}_amd64.AppImage
./win11-clipboard-history_${{ steps.get_version.outputs.VERSION }}_amd64.AppImage
```
### Changelog
See the [commit history](https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux/commits/${{ github.ref_name }}) for changes.
draft: false
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-linux:
name: Build Linux
runs-on: ubuntu-latest
needs: create-release
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Rust
uses: dtolnay/rust-action@stable
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
src-tauri/target/
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file \
libssl-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev \
libgtk-3-dev libglib2.0-dev
- name: Install npm dependencies
run: npm ci
- name: Build Tauri app
run: npm run tauri:build
- name: Upload DEB to Release
uses: softprops/action-gh-release@v1
with:
files: src-tauri/target/release/bundle/deb/*.deb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload AppImage to Release
uses: softprops/action-gh-release@v1
with:
files: src-tauri/target/release/bundle/appimage/*.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload RPM to Release
uses: softprops/action-gh-release@v1
with:
files: src-tauri/target/release/bundle/rpm/*.rpm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

40
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Stale Issue Handler
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: |
This issue has been automatically marked as stale because it has not had recent activity.
It will be closed in 14 days if no further activity occurs.
If this issue is still relevant, please comment or update it to keep it open.
stale-pr-message: |
This pull request has been automatically marked as stale because it has not had recent activity.
It will be closed in 14 days if no further activity occurs.
Please rebase and update your PR if you'd like to continue with it.
close-issue-message: |
This issue has been closed due to inactivity.
Feel free to reopen if you believe this issue is still relevant.
close-pr-message: |
This pull request has been closed due to inactivity.
Feel free to reopen or create a new PR if you'd like to continue.
days-before-stale: 60
days-before-close: 14
stale-issue-label: 'stale'
stale-pr-label: 'stale'
exempt-issue-labels: 'pinned,security,bug,help wanted'
exempt-pr-labels: 'pinned,security'
exempt-all-milestones: true

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Dependencies
node_modules/
target/
# Build outputs
dist/
src-tauri/target/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Environment
.env
.env.local
.env.*.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Test
coverage/
# Tauri
src-tauri/WixTools/
src-tauri/Cargo.lock

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"plugins": []
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Win11 Clipboard History Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

269
Makefile Normal file
View File

@@ -0,0 +1,269 @@
# Win11 Clipboard History - Makefile
# Cross-distro build and install for Ubuntu, Debian, Fedora, and Arch Linux
SHELL := /bin/bash
APP_NAME := win11-clipboard-history
VERSION := 1.0.0
PREFIX ?= /usr/local
BINDIR := $(PREFIX)/bin
DATADIR := $(PREFIX)/share
DESTDIR ?=
# Detect distro
DISTRO := $(shell \
if [ -f /etc/os-release ]; then \
. /etc/os-release && echo $$ID; \
elif [ -f /etc/debian_version ]; then \
echo "debian"; \
elif [ -f /etc/fedora-release ]; then \
echo "fedora"; \
elif [ -f /etc/arch-release ]; then \
echo "arch"; \
else \
echo "unknown"; \
fi)
# Colors for pretty output
CYAN := \033[0;36m
GREEN := \033[0;32m
YELLOW := \033[0;33m
RED := \033[0;31m
RESET := \033[0m
.PHONY: all help deps deps-ubuntu deps-debian deps-fedora deps-arch \
rust node check-deps dev build install uninstall clean run \
lint format test
all: build
help:
@echo -e "$(CYAN)╔════════════════════════════════════════════════════════════════╗$(RESET)"
@echo -e "$(CYAN)║ Win11 Clipboard History - Build Commands ║$(RESET)"
@echo -e "$(CYAN)╚════════════════════════════════════════════════════════════════╝$(RESET)"
@echo ""
@echo -e "$(GREEN)Setup:$(RESET)"
@echo " make deps - Install system dependencies (auto-detect distro)"
@echo " make deps-ubuntu - Install dependencies for Ubuntu"
@echo " make deps-debian - Install dependencies for Debian"
@echo " make deps-fedora - Install dependencies for Fedora"
@echo " make deps-arch - Install dependencies for Arch Linux"
@echo " make rust - Install Rust via rustup"
@echo " make node - Install Node.js via nvm"
@echo ""
@echo -e "$(GREEN)Development:$(RESET)"
@echo " make dev - Run in development mode (hot reload)"
@echo " make run - Run the development version (clean env)"
@echo " make build - Build production release"
@echo " make lint - Run linters"
@echo " make format - Format code"
@echo ""
@echo -e "$(GREEN)Installation:$(RESET)"
@echo " make install - Install to system (requires sudo)"
@echo " make uninstall - Remove from system (requires sudo)"
@echo ""
@echo -e "$(GREEN)Maintenance:$(RESET)"
@echo " make clean - Remove build artifacts"
@echo " make check-deps - Verify all dependencies are installed"
@echo ""
@echo -e "$(YELLOW)Detected distro: $(DISTRO)$(RESET)"
# ============================================================================
# Dependencies
# ============================================================================
deps:
@echo -e "$(CYAN)Detected distribution: $(DISTRO)$(RESET)"
ifeq ($(DISTRO),ubuntu)
@$(MAKE) deps-ubuntu
else ifeq ($(DISTRO),debian)
@$(MAKE) deps-debian
else ifeq ($(DISTRO),fedora)
@$(MAKE) deps-fedora
else ifeq ($(DISTRO),arch)
@$(MAKE) deps-arch
else ifeq ($(DISTRO),manjaro)
@$(MAKE) deps-arch
else ifeq ($(DISTRO),endeavouros)
@$(MAKE) deps-arch
else ifeq ($(DISTRO),linuxmint)
@$(MAKE) deps-ubuntu
else ifeq ($(DISTRO),pop)
@$(MAKE) deps-ubuntu
else
@echo -e "$(RED)Unknown distribution: $(DISTRO)$(RESET)"
@echo "Please install dependencies manually. See README.md"
@exit 1
endif
deps-ubuntu deps-debian:
@echo -e "$(CYAN)Installing dependencies for Ubuntu/Debian...$(RESET)"
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libxdo-dev \
libgtk-3-dev \
libglib2.0-dev \
pkg-config
@echo -e "$(GREEN)✓ Dependencies installed successfully$(RESET)"
deps-fedora:
@echo -e "$(CYAN)Installing dependencies for Fedora...$(RESET)"
sudo dnf install -y \
webkit2gtk4.1-devel \
openssl-devel \
curl \
wget \
file \
libappindicator-gtk3-devel \
librsvg2-devel \
libxdo-devel \
gtk3-devel \
glib2-devel \
pkg-config \
@development-tools
@echo -e "$(GREEN)✓ Dependencies installed successfully$(RESET)"
deps-arch:
@echo -e "$(CYAN)Installing dependencies for Arch Linux...$(RESET)"
sudo pacman -Syu --needed --noconfirm \
webkit2gtk-4.1 \
base-devel \
curl \
wget \
file \
openssl \
libappindicator-gtk3 \
librsvg \
xdotool \
gtk3 \
glib2 \
pkgconf
@echo -e "$(GREEN)✓ Dependencies installed successfully$(RESET)"
rust:
@echo -e "$(CYAN)Installing Rust via rustup...$(RESET)"
@if command -v rustc &> /dev/null; then \
echo -e "$(YELLOW)Rust is already installed: $$(rustc --version)$(RESET)"; \
else \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; \
echo -e "$(GREEN)✓ Rust installed. Run 'source ~/.cargo/env' to update PATH$(RESET)"; \
fi
node:
@echo -e "$(CYAN)Installing Node.js via nvm...$(RESET)"
@if command -v node &> /dev/null; then \
echo -e "$(YELLOW)Node.js is already installed: $$(node --version)$(RESET)"; \
else \
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash; \
export NVM_DIR="$$HOME/.nvm"; \
[ -s "$$NVM_DIR/nvm.sh" ] && \. "$$NVM_DIR/nvm.sh"; \
nvm install 20; \
echo -e "$(GREEN)✓ Node.js installed$(RESET)"; \
fi
check-deps:
@echo -e "$(CYAN)Checking dependencies...$(RESET)"
@echo ""
@echo "System tools:"
@command -v rustc &> /dev/null && echo -e " $(GREEN)$(RESET) Rust: $$(rustc --version)" || echo -e " $(RED)$(RESET) Rust: not found"
@command -v cargo &> /dev/null && echo -e " $(GREEN)$(RESET) Cargo: $$(cargo --version)" || echo -e " $(RED)$(RESET) Cargo: not found"
@command -v node &> /dev/null && echo -e " $(GREEN)$(RESET) Node.js: $$(node --version)" || echo -e " $(RED)$(RESET) Node.js: not found"
@command -v npm &> /dev/null && echo -e " $(GREEN)$(RESET) npm: $$(npm --version)" || echo -e " $(RED)$(RESET) npm: not found"
@echo ""
@echo "Libraries:"
@pkg-config --exists webkit2gtk-4.1 2>/dev/null && echo -e " $(GREEN)$(RESET) webkit2gtk-4.1" || echo -e " $(RED)$(RESET) webkit2gtk-4.1"
@pkg-config --exists gtk+-3.0 2>/dev/null && echo -e " $(GREEN)$(RESET) gtk+-3.0" || echo -e " $(RED)$(RESET) gtk+-3.0"
@pkg-config --exists glib-2.0 2>/dev/null && echo -e " $(GREEN)$(RESET) glib-2.0" || echo -e " $(RED)$(RESET) glib-2.0"
@pkg-config --exists openssl 2>/dev/null && echo -e " $(GREEN)$(RESET) openssl" || echo -e " $(RED)$(RESET) openssl"
@echo ""
# ============================================================================
# Development
# ============================================================================
dev: node_modules
@echo -e "$(CYAN)Starting development server...$(RESET)"
@./scripts/run-dev.sh
run: node_modules
@echo -e "$(CYAN)Running with clean environment...$(RESET)"
@./scripts/run-dev.sh
node_modules: package.json
@echo -e "$(CYAN)Installing npm dependencies...$(RESET)"
npm install
@touch node_modules
# ============================================================================
# Build
# ============================================================================
build: node_modules
@echo -e "$(CYAN)Building production release...$(RESET)"
npm run tauri:build
@echo -e "$(GREEN)✓ Build complete!$(RESET)"
@echo -e "$(YELLOW)Packages available in: src-tauri/target/release/bundle/$(RESET)"
# ============================================================================
# Install / Uninstall
# ============================================================================
install: build
@echo -e "$(CYAN)Installing $(APP_NAME)...$(RESET)"
install -Dm755 src-tauri/target/release/$(APP_NAME) $(DESTDIR)$(BINDIR)/$(APP_NAME)
install -Dm644 src-tauri/icons/128x128.png $(DESTDIR)$(DATADIR)/icons/hicolor/128x128/apps/$(APP_NAME).png
install -Dm644 src-tauri/icons/icon.png $(DESTDIR)$(DATADIR)/icons/hicolor/256x256/apps/$(APP_NAME).png
@# Create desktop entry
@mkdir -p $(DESTDIR)$(DATADIR)/applications
@echo "[Desktop Entry]" > $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "Name=Clipboard History" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "Comment=Windows 11-style Clipboard History Manager" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "Exec=$(BINDIR)/$(APP_NAME)" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "Icon=$(APP_NAME)" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "Terminal=false" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "Type=Application" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "Categories=Utility;" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "Keywords=clipboard;history;paste;copy;" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo "StartupWMClass=$(APP_NAME)" >> $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo -e "$(GREEN)✓ Installed successfully$(RESET)"
@echo -e "$(YELLOW)You may need to log out and back in for the desktop entry to appear$(RESET)"
uninstall:
@echo -e "$(CYAN)Uninstalling $(APP_NAME)...$(RESET)"
rm -f $(DESTDIR)$(BINDIR)/$(APP_NAME)
rm -f $(DESTDIR)$(DATADIR)/icons/hicolor/128x128/apps/$(APP_NAME).png
rm -f $(DESTDIR)$(DATADIR)/icons/hicolor/256x256/apps/$(APP_NAME).png
rm -f $(DESTDIR)$(DATADIR)/applications/$(APP_NAME).desktop
@echo -e "$(GREEN)✓ Uninstalled successfully$(RESET)"
# ============================================================================
# Code Quality
# ============================================================================
lint:
@echo -e "$(CYAN)Running linters...$(RESET)"
npm run lint
cd src-tauri && cargo clippy -- -D warnings
format:
@echo -e "$(CYAN)Formatting code...$(RESET)"
npm run format
cd src-tauri && cargo fmt
# ============================================================================
# Clean
# ============================================================================
clean:
@echo -e "$(CYAN)Cleaning build artifacts...$(RESET)"
rm -rf node_modules
rm -rf dist
rm -rf src-tauri/target
@echo -e "$(GREEN)✓ Cleaned$(RESET)"

360
README.md Normal file
View File

@@ -0,0 +1,360 @@
# 📋 Win11 Clipboard History
<div align="center">
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Rust](https://img.shields.io/badge/rust-1.70+-orange.svg)
![Tauri](https://img.shields.io/badge/tauri-v2-blue.svg)
![Platform](https://img.shields.io/badge/platform-linux-lightgrey.svg)
**A beautiful, Windows 11-style Clipboard History Manager for Linux**
Built with 🦀 Rust + ⚡ Tauri v2 + ⚛️ React + 🎨 Tailwind CSS
[Features](#-features) • [Installation](#-installation) • [Development](#-development) • [Contributing](#-contributing)
</div>
---
## ✨ Features
- 🎨 **Windows 11 Design** - Pixel-perfect recreation of the Win+V clipboard UI with Acrylic/Mica glassmorphism effects
- 🌙 **Dark/Light Mode** - Automatically detects system theme preference
- ⌨️ **Global Hotkey** - Press `Super+V` or `Ctrl+Alt+V` to open from anywhere
- 📌 **Pin Items** - Keep important clipboard entries at the top
- 🖼️ **Image Support** - Copy and paste images with preview thumbnails
- 🚀 **Blazing Fast** - Written in Rust for maximum performance
- 🔒 **Privacy First** - All data stays local on your machine
- 🖱️ **Smart Positioning** - Window appears at your cursor position
- 💨 **System Tray** - Runs silently in the background
- 🐧 **Wayland & X11** - Works on both display servers
## 📦 Installation
### Quick Start (Recommended)
The easiest way to get started:
```bash
# Clone the repository
git clone https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux.git
cd win11-clipboard-history
# Install system dependencies (auto-detects your distro)
make deps
# Install Rust and Node.js if needed
make rust
make node
source ~/.cargo/env # Reload shell environment
# Build and install
make build
sudo make install
```
### Distribution-Specific Dependencies
<details>
<summary><b>🟠 Ubuntu / Debian / Linux Mint / Pop!_OS</b></summary>
```bash
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libxdo-dev \
libgtk-3-dev \
libglib2.0-dev \
pkg-config
```
</details>
<details>
<summary><b>🔵 Fedora</b></summary>
```bash
sudo dnf install -y \
webkit2gtk4.1-devel \
openssl-devel \
curl \
wget \
file \
libappindicator-gtk3-devel \
librsvg2-devel \
libxdo-devel \
gtk3-devel \
glib2-devel \
pkg-config \
@development-tools
```
</details>
<details>
<summary><b>🟣 Arch Linux / Manjaro / EndeavourOS</b></summary>
```bash
sudo pacman -Syu --needed \
webkit2gtk-4.1 \
base-devel \
curl \
wget \
file \
openssl \
libappindicator-gtk3 \
librsvg \
xdotool \
gtk3 \
glib2 \
pkgconf
```
</details>
### Install Rust
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
```
### Install Node.js (v18+)
```bash
# Using nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.nvm/nvm.sh
nvm install 20
nvm use 20
```
### Build from Source
```bash
# Clone the repository
git clone https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux.git
cd win11-clipboard-history
# Install npm dependencies
npm install
# Build the application
npm run tauri:build
# The built packages will be in:
# - Binary: src-tauri/target/release/win11-clipboard-history
# - DEB package: src-tauri/target/release/bundle/deb/
# - RPM package: src-tauri/target/release/bundle/rpm/
# - AppImage: src-tauri/target/release/bundle/appimage/
```
## 🛠️ Development
### Quick Start
```bash
# Install dependencies
npm install
# Run in development mode (hot reload enabled)
make dev
# OR
./scripts/run-dev.sh
```
> **Note for VS Code Snap users**: If you're using VS Code installed via Snap, use `make dev` or `./scripts/run-dev.sh` instead of `npm run tauri:dev` directly. This script cleans the environment to avoid library conflicts.
### Makefile Commands
| Command | Description |
|---------|-------------|
| `make help` | Show all available commands |
| `make deps` | Install system dependencies (auto-detect distro) |
| `make deps-ubuntu` | Install dependencies for Ubuntu/Debian |
| `make deps-fedora` | Install dependencies for Fedora |
| `make deps-arch` | Install dependencies for Arch Linux |
| `make rust` | Install Rust via rustup |
| `make node` | Install Node.js via nvm |
| `make check-deps` | Verify all dependencies are installed |
| `make dev` | Run in development mode |
| `make build` | Build production release |
| `make install` | Install to system (requires sudo) |
| `make uninstall` | Remove from system (requires sudo) |
| `make clean` | Remove build artifacts |
| `make lint` | Run linters |
| `make format` | Format code |
### npm Scripts
| Command | Description |
|---------|-------------|
| `npm run dev` | Start Vite dev server (frontend only) |
| `npm run tauri:dev` | Start full Tauri development mode |
| `npm run tauri:build` | Build production release |
| `npm run build` | Build frontend only |
| `npm run lint` | Run ESLint |
| `npm run format` | Format code with Prettier |
### Project Structure
```
win11-clipboard-history/
├── src/ # React frontend
│ ├── components/ # UI components
│ │ ├── EmptyState.tsx # Empty history state
│ │ ├── Header.tsx # App header with actions
│ │ ├── HistoryItem.tsx # Clipboard item card
│ │ └── TabBar.tsx # Tab navigation
│ ├── hooks/ # React hooks
│ │ ├── useClipboardHistory.ts
│ │ └── useDarkMode.ts
│ ├── types/ # TypeScript types
│ │ └── clipboard.ts
│ ├── App.tsx # Main app component
│ ├── index.css # Global styles + Tailwind
│ └── main.tsx # Entry point
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── main.rs # App setup, tray, commands
│ │ ├── lib.rs # Library exports
│ │ ├── clipboard_manager.rs # Clipboard operations
│ │ └── hotkey_manager.rs # Global shortcuts
│ ├── capabilities/ # Tauri permissions
│ ├── icons/ # App icons
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # Tauri configuration
├── scripts/
│ └── run-dev.sh # Clean environment dev script
├── Makefile # Build automation
├── tailwind.config.js # Win11 theme config
├── vite.config.ts # Vite configuration
└── package.json # Node dependencies
```
### Global Hotkey Permissions
On Linux with X11, global keyboard capture may require the user to be in the `input` group:
```bash
sudo usermod -aG input $USER
# Log out and back in for changes to take effect
```
On Wayland, permissions are typically handled automatically by the compositor.
## 🐧 Platform Support
### Display Servers
| Display Server | Status | Notes |
|----------------|--------|-------|
| X11 | ✅ Full support | Global hotkeys work via rdev |
| Wayland | ✅ Full support | Uses wl-clipboard for clipboard access |
### Tested Distributions
| Distribution | Version | Status |
|--------------|---------|--------|
| Ubuntu | 22.04+ | ✅ Tested |
| Debian | 12+ | ✅ Tested |
| Fedora | 38+ | ✅ Tested |
| Arch Linux | Rolling | ✅ Tested |
| Manjaro | Latest | ✅ Tested |
| Linux Mint | 21+ | ✅ Tested |
| Pop!_OS | 22.04+ | ✅ Tested |
## 🎨 Customization
### Changing the Hotkey
Edit `src-tauri/src/hotkey_manager.rs` to modify the global shortcut:
```rust
// Current: Super+V or Ctrl+Alt+V
Key::KeyV => {
if super_pressed || (ctrl_pressed && alt_pressed) {
callback();
}
}
```
### Theme Colors
The Windows 11 color palette is defined in `tailwind.config.js`:
```js
colors: {
win11: {
'bg-primary': '#202020',
'bg-accent': '#0078d4',
// ... customize as needed
}
}
```
## 🔧 Troubleshooting
### Application won't start
1. **Check dependencies**: Run `make check-deps` to verify all dependencies are installed
2. **Wayland clipboard issues**: Ensure `wl-clipboard` is installed for Wayland support
3. **VS Code Snap conflict**: Use `make dev` or `./scripts/run-dev.sh` instead of `npm run tauri:dev`
### Global hotkey not working
1. **X11**: Add user to input group: `sudo usermod -aG input $USER`
2. **Wayland**: Some compositors may require additional permissions
3. Try alternative hotkey `Ctrl+Alt+V` instead of `Super+V`
### Window not showing at cursor position
This may occur on some Wayland compositors. The window will fallback to a default position.
## 🤝 Contributing
Contributions are welcome! Here's how you can help:
1. **Fork** the repository
2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
3. **Commit** your changes (`git commit -m 'Add amazing feature'`)
4. **Push** to the branch (`git push origin feature/amazing-feature`)
5. **Open** a Pull Request
### Development Guidelines
- Follow the existing code style
- Run `make lint` and `make format` before committing
- Write meaningful commit messages
- Add tests for new features
- Update documentation as needed
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- [Tauri](https://tauri.app/) - For the amazing Rust-based framework
- [Windows 11](https://www.microsoft.com/windows/windows-11) - For the beautiful design inspiration
- [rdev](https://github.com/Narsil/rdev) - For global keyboard capture
- [arboard](https://github.com/1Password/arboard) - For cross-platform clipboard access
- [wl-clipboard-rs](https://github.com/YaLTeR/wl-clipboard-rs) - For Wayland clipboard support
---
<div align="center">
**If you find this project useful, please consider giving it a ⭐!**
Made with ❤️ for the Linux community
</div>

29
eslint.config.js Normal file
View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist', 'src-tauri'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
}
)

24
index.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clipboard History</title>
<style>
/* Prevent flash of white during load */
html {
background: transparent;
}
body {
margin: 0;
padding: 0;
background: transparent;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4326
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

57
package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "win11-clipboard-history",
"private": true,
"version": "1.0.0",
"type": "module",
"description": "A Windows 11-style Clipboard History Manager for Linux built with Tauri v2",
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux"
},
"keywords": [
"clipboard",
"linux",
"tauri",
"windows-11",
"clipboard-manager",
"rust"
],
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\""
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"clsx": "^2.1.0",
"lucide-react": "^0.460.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.13.0",
"@typescript-eslint/parser": "^8.49.0",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.14.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.15",
"typescript": "^5.6.3",
"typescript-eslint": "^8.49.0",
"vite": "^5.4.11"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

14
public/vite.svg Normal file
View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0078d4"/>
<stop offset="100%" style="stop-color:#005a9e"/>
</linearGradient>
</defs>
<rect width="32" height="32" rx="6" fill="url(#bg)"/>
<rect x="8" y="6" width="16" height="20" rx="2" fill="white" opacity="0.95"/>
<rect x="11" y="4" width="10" height="4" rx="1" fill="white"/>
<rect x="10" y="12" width="12" height="2" rx="1" fill="#0078d4" opacity="0.5"/>
<rect x="10" y="16" width="10" height="2" rx="1" fill="#0078d4" opacity="0.4"/>
<rect x="10" y="20" width="8" height="2" rx="1" fill="#0078d4" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 751 B

19
scripts/run-dev.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Clean environment script to avoid snap library conflicts
# Reset problematic environment variables
unset GIO_MODULE_DIR
unset GTK_PATH
unset GTK_IM_MODULE_FILE
unset GTK_EXE_PREFIX
unset LOCPATH
unset GSETTINGS_SCHEMA_DIR
# Use system XDG_DATA_DIRS
export XDG_DATA_DIRS="/usr/local/share:/usr/share:/var/lib/snapd/desktop"
# Change to project directory
cd "$(dirname "$0")/.."
# Run tauri dev
npm run tauri:dev

63
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,63 @@
[package]
name = "win11-clipboard-history"
version = "1.0.0"
description = "A Windows 11-style Clipboard History Manager for Linux"
authors = ["Your Name"]
license = "MIT"
repository = "https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux"
edition = "2021"
rust-version = "1.70"
[lib]
name = "win11_clipboard_history_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "win11-clipboard-history"
path = "src/main.rs"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
# Tauri Core
tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-shell = "2"
# Clipboard Management - wayland-data-control for native Wayland support
arboard = { version = "3.4", features = ["image-data", "wayland-data-control"] }
# Global Hotkeys
rdev = { version = "0.5", features = ["unstable_grab"] }
# Async Runtime
tokio = { version = "1", features = ["full"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Image Handling
image = "0.25"
base64 = "0.22"
# Utilities
once_cell = "1.19"
parking_lot = "0.12"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.10", features = ["v4", "serde"] }
# X11 Simulation for paste injection (Linux)
[target.'cfg(target_os = "linux")'.dependencies]
enigo = "0.2"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
opt-level = "z"
strip = true

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,29 @@
{
"$schema": "https://schema.tauri.app/config/2",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-close",
"core:window:allow-hide",
"core:window:allow-show",
"core:window:allow-set-focus",
"core:window:allow-set-position",
"core:window:allow-set-size",
"core:window:allow-center",
"core:window:allow-is-visible",
"core:event:default",
"core:event:allow-emit",
"core:event:allow-listen",
"core:app:default",
"core:tray:default",
"core:tray:allow-set-icon",
"core:tray:allow-set-tooltip",
"core:resources:default",
"shell:default",
"shell:allow-open"
],
"platforms": ["linux", "windows", "macOS"]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:default","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-set-focus","core:window:allow-set-position","core:window:allow-set-size","core:window:allow-center","core:window:allow-is-visible","core:event:default","core:event:allow-emit","core:event:allow-listen","core:app:default","core:tray:default","core:tray:allow-set-icon","core:tray:allow-set-tooltip","core:resources:default","shell:default","shell:allow-open"],"platforms":["linux","windows","macOS"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

28
src-tauri/icons/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Icon Generation
To generate the required icon files, you can use ImageMagick:
```bash
# Install ImageMagick if not already installed
# Ubuntu/Debian: sudo apt install imagemagick
# Fedora: sudo dnf install ImageMagick
# Arch: sudo pacman -S imagemagick
# Generate PNG icons from SVG
convert -background transparent icon.svg -resize 32x32 32x32.png
convert -background transparent icon.svg -resize 128x128 128x128.png
convert -background transparent icon.svg -resize 256x256 128x128@2x.png
convert -background transparent icon.svg -resize 128x128 icon.png
# Generate ICO for Windows (if needed)
convert icon.svg -resize 256x256 -define icon:auto-resize=256,128,64,48,32,16 icon.ico
# Generate ICNS for macOS (if needed)
# You'll need to create an iconset folder and use iconutil on macOS
```
Alternatively, use an online tool like:
- https://realfavicongenerator.net/
- https://cloudconvert.com/svg-to-png
For now, placeholder PNG files are included. Replace them with properly generated icons.

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

20
src-tauri/icons/icon.svg Normal file
View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0078d4"/>
<stop offset="100%" style="stop-color:#005a9e"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="128" height="128" rx="24" fill="url(#bg)"/>
<!-- Clipboard body -->
<rect x="32" y="24" width="64" height="80" rx="6" fill="white" opacity="0.95"/>
<!-- Clipboard clip -->
<rect x="44" y="16" width="40" height="16" rx="4" fill="white"/>
<rect x="52" y="20" width="24" height="8" rx="2" fill="#0078d4"/>
<!-- Lines on clipboard -->
<rect x="42" y="48" width="44" height="4" rx="2" fill="#0078d4" opacity="0.6"/>
<rect x="42" y="60" width="36" height="4" rx="2" fill="#0078d4" opacity="0.4"/>
<rect x="42" y="72" width="40" height="4" rx="2" fill="#0078d4" opacity="0.3"/>
<rect x="42" y="84" width="28" height="4" rx="2" fill="#0078d4" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 1017 B

View File

@@ -0,0 +1,288 @@
//! Clipboard Manager Module
//! Handles clipboard monitoring, history storage, and paste injection
use arboard::{Clipboard, ImageData};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use chrono::{DateTime, Utc};
use image::{DynamicImage, ImageFormat};
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::Cursor;
use uuid::Uuid;
/// Maximum number of items to store in history
const MAX_HISTORY_SIZE: usize = 50;
/// Content type for clipboard items
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "data")]
pub enum ClipboardContent {
/// Plain text content
Text(String),
/// Image as base64 encoded PNG
Image {
base64: String,
width: u32,
height: u32,
},
}
/// A single clipboard history item
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipboardItem {
/// Unique identifier
pub id: String,
/// The content
pub content: ClipboardContent,
/// When it was copied
pub timestamp: DateTime<Utc>,
/// Whether this item is pinned
pub pinned: bool,
/// Preview text (for display)
pub preview: String,
}
impl ClipboardItem {
/// Create a new text item
pub fn new_text(text: String) -> Self {
let preview = if text.len() > 100 {
format!("{}...", &text[..100])
} else {
text.clone()
};
Self {
id: Uuid::new_v4().to_string(),
content: ClipboardContent::Text(text),
timestamp: Utc::now(),
pinned: false,
preview,
}
}
/// Create a new image item
pub fn new_image(base64: String, width: u32, height: u32) -> Self {
Self {
id: Uuid::new_v4().to_string(),
content: ClipboardContent::Image {
base64,
width,
height,
},
timestamp: Utc::now(),
pinned: false,
preview: format!("Image ({}x{})", width, height),
}
}
}
/// Manages clipboard operations and history
pub struct ClipboardManager {
history: Vec<ClipboardItem>,
}
impl ClipboardManager {
/// Create a new clipboard manager
pub fn new() -> Self {
Self {
history: Vec::with_capacity(MAX_HISTORY_SIZE),
}
}
/// Get a clipboard instance (creates new each time for thread safety)
fn get_clipboard() -> Result<Clipboard, arboard::Error> {
Clipboard::new()
}
/// Get current text from clipboard
pub fn get_current_text(&mut self) -> Result<String, arboard::Error> {
Self::get_clipboard()?.get_text()
}
/// Get current image from clipboard with hash for change detection
pub fn get_current_image(
&mut self,
) -> Result<Option<(ImageData<'static>, u64)>, arboard::Error> {
let mut clipboard = Self::get_clipboard()?;
match clipboard.get_image() {
Ok(image) => {
// Create hash from image data for comparison
let mut hasher = DefaultHasher::new();
image.bytes.hash(&mut hasher);
let hash = hasher.finish();
// Convert to owned data
let owned = ImageData {
width: image.width,
height: image.height,
bytes: image.bytes.into_owned().into(),
};
Ok(Some((owned, hash)))
}
Err(arboard::Error::ContentNotAvailable) => Ok(None),
Err(e) => Err(e),
}
}
/// Add text to history
pub fn add_text(&mut self, text: String) -> Option<ClipboardItem> {
// Don't add empty strings or duplicates
if text.trim().is_empty() {
return None;
}
// Check for duplicates (non-pinned items only)
if let Some(pos) = self.history.iter().position(|item| {
!item.pinned && matches!(&item.content, ClipboardContent::Text(t) if t == &text)
}) {
// Remove the duplicate and add to top
self.history.remove(pos);
}
let item = ClipboardItem::new_text(text);
self.insert_item(item.clone());
Some(item)
}
/// Add image to history
pub fn add_image(&mut self, image_data: ImageData<'_>) -> Option<ClipboardItem> {
// Convert to base64 PNG
let img = DynamicImage::ImageRgba8(
image::RgbaImage::from_raw(
image_data.width as u32,
image_data.height as u32,
image_data.bytes.to_vec(),
)
.unwrap(),
);
let mut buffer = Cursor::new(Vec::new());
if img.write_to(&mut buffer, ImageFormat::Png).is_err() {
return None;
}
let base64 = BASE64.encode(buffer.get_ref());
let item =
ClipboardItem::new_image(base64, image_data.width as u32, image_data.height as u32);
self.insert_item(item.clone());
Some(item)
}
/// Insert an item at the top of history (respecting pinned items)
fn insert_item(&mut self, item: ClipboardItem) {
// Find the first non-pinned position
let insert_pos = self.history.iter().position(|i| !i.pinned).unwrap_or(0);
self.history.insert(insert_pos, item);
// Trim to max size (remove from end, but preserve pinned items)
while self.history.len() > MAX_HISTORY_SIZE {
if let Some(pos) = self.history.iter().rposition(|i| !i.pinned) {
self.history.remove(pos);
} else {
break; // All items are pinned, don't remove any
}
}
}
/// Get the full history
pub fn get_history(&self) -> Vec<ClipboardItem> {
self.history.clone()
}
/// Get a specific item by ID
pub fn get_item(&self, id: &str) -> Option<&ClipboardItem> {
self.history.iter().find(|item| item.id == id)
}
/// Clear all non-pinned history
pub fn clear(&mut self) {
self.history.retain(|item| item.pinned);
}
/// Remove a specific item
pub fn remove_item(&mut self, id: &str) {
self.history.retain(|item| item.id != id);
}
/// Toggle pin status
pub fn toggle_pin(&mut self, id: &str) -> Option<ClipboardItem> {
if let Some(item) = self.history.iter_mut().find(|i| i.id == id) {
item.pinned = !item.pinned;
return Some(item.clone());
}
None
}
/// Paste an item (write to clipboard and simulate Ctrl+V)
pub fn paste_item(&self, item: &ClipboardItem) -> Result<(), String> {
// Create a new clipboard instance for pasting
let mut clipboard = Self::get_clipboard().map_err(|e| e.to_string())?;
match &item.content {
ClipboardContent::Text(text) => {
clipboard.set_text(text).map_err(|e| e.to_string())?;
}
ClipboardContent::Image {
base64,
width,
height,
} => {
let bytes = BASE64.decode(base64).map_err(|e| e.to_string())?;
let img = image::load_from_memory(&bytes).map_err(|e| e.to_string())?;
let rgba = img.to_rgba8();
let image_data = ImageData {
width: *width as usize,
height: *height as usize,
bytes: rgba.into_raw().into(),
};
clipboard.set_image(image_data).map_err(|e| e.to_string())?;
}
}
// Simulate Ctrl+V to paste
simulate_paste()?;
Ok(())
}
}
/// Simulate Ctrl+V keypress for paste injection
#[cfg(target_os = "linux")]
fn simulate_paste() -> Result<(), String> {
use enigo::{Direction, Enigo, Key, Keyboard, Settings};
// Small delay to ensure clipboard is ready
std::thread::sleep(std::time::Duration::from_millis(50));
let mut enigo = Enigo::new(&Settings::default()).map_err(|e| e.to_string())?;
// Press Ctrl+V
enigo
.key(Key::Control, Direction::Press)
.map_err(|e| e.to_string())?;
enigo
.key(Key::Unicode('v'), Direction::Click)
.map_err(|e| e.to_string())?;
enigo
.key(Key::Control, Direction::Release)
.map_err(|e| e.to_string())?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn simulate_paste() -> Result<(), String> {
// Fallback for other platforms - just set clipboard
Ok(())
}
impl Default for ClipboardManager {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,108 @@
//! Global Hotkey Manager Module
//! Handles global keyboard shortcuts using rdev
use rdev::{listen, Event, EventType, Key};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
/// Manages global hotkey listening
pub struct HotkeyManager {
running: Arc<AtomicBool>,
_handle: Option<JoinHandle<()>>,
}
impl HotkeyManager {
/// Create a new hotkey manager with a callback for when the hotkey is pressed
pub fn new<F>(callback: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let callback = Arc::new(callback);
let handle = thread::spawn(move || {
// Use atomic bools for thread-safe state tracking
let super_pressed = Arc::new(AtomicBool::new(false));
let ctrl_pressed = Arc::new(AtomicBool::new(false));
let alt_pressed = Arc::new(AtomicBool::new(false));
let super_clone = super_pressed.clone();
let ctrl_clone = ctrl_pressed.clone();
let alt_clone = alt_pressed.clone();
let callback_clone = callback.clone();
let running_inner = running_clone.clone();
// Use listen for better compatibility (doesn't require special permissions)
let result = listen(move |event: Event| {
if !running_inner.load(Ordering::SeqCst) {
return;
}
match event.event_type {
EventType::KeyPress(key) => {
match key {
Key::MetaLeft | Key::MetaRight => {
super_clone.store(true, Ordering::SeqCst);
}
Key::ControlLeft | Key::ControlRight => {
ctrl_clone.store(true, Ordering::SeqCst);
}
Key::Alt | Key::AltGr => {
alt_clone.store(true, Ordering::SeqCst);
}
Key::KeyV => {
// Check for Super+V (Windows-like) or Ctrl+Alt+V (fallback)
let super_down = super_clone.load(Ordering::SeqCst);
let ctrl_down = ctrl_clone.load(Ordering::SeqCst);
let alt_down = alt_clone.load(Ordering::SeqCst);
if super_down || (ctrl_down && alt_down) {
callback_clone();
}
}
_ => {}
}
}
EventType::KeyRelease(key) => match key {
Key::MetaLeft | Key::MetaRight => {
super_clone.store(false, Ordering::SeqCst);
}
Key::ControlLeft | Key::ControlRight => {
ctrl_clone.store(false, Ordering::SeqCst);
}
Key::Alt | Key::AltGr => {
alt_clone.store(false, Ordering::SeqCst);
}
_ => {}
},
_ => {}
}
});
if let Err(e) = result {
eprintln!("Hotkey listener error: {:?}", e);
eprintln!("Note: Global hotkeys may require the user to be in the 'input' group on Linux.");
eprintln!("Run: sudo usermod -aG input $USER");
}
});
Self {
running,
_handle: Some(handle),
}
}
/// Stop the hotkey listener
#[allow(dead_code)]
pub fn stop(&self) {
self.running.store(false, Ordering::SeqCst);
}
}
impl Drop for HotkeyManager {
fn drop(&mut self) {
self.stop();
}
}

8
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,8 @@
//! Win11 Clipboard History Library
//! This module re-exports the core functionality for use as a library
pub mod clipboard_manager;
pub mod hotkey_manager;
pub use clipboard_manager::{ClipboardContent, ClipboardItem, ClipboardManager};
pub use hotkey_manager::HotkeyManager;

249
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,249 @@
// Prevents additional console window on Windows in release
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use parking_lot::Mutex;
use std::sync::Arc;
use tauri::{
image::Image,
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
AppHandle, Emitter, Manager, State, WebviewWindow,
};
use win11_clipboard_history_lib::clipboard_manager::{ClipboardItem, ClipboardManager};
use win11_clipboard_history_lib::hotkey_manager::HotkeyManager;
/// Application state shared across all handlers
pub struct AppState {
clipboard_manager: Arc<Mutex<ClipboardManager>>,
hotkey_manager: Arc<Mutex<Option<HotkeyManager>>>,
}
/// Get clipboard history
#[tauri::command]
fn get_history(state: State<AppState>) -> Vec<ClipboardItem> {
state.clipboard_manager.lock().get_history()
}
/// Clear all clipboard history
#[tauri::command]
fn clear_history(state: State<AppState>) {
state.clipboard_manager.lock().clear();
}
/// Delete a specific item from history
#[tauri::command]
fn delete_item(state: State<AppState>, id: String) {
state.clipboard_manager.lock().remove_item(&id);
}
/// Pin/unpin an item
#[tauri::command]
fn toggle_pin(state: State<AppState>, id: String) -> Option<ClipboardItem> {
state.clipboard_manager.lock().toggle_pin(&id)
}
/// Paste an item from history
#[tauri::command]
async fn paste_item(app: AppHandle, state: State<'_, AppState>, id: String) -> Result<(), String> {
// First hide the window
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
// Get the item and paste it
let item = {
let manager = state.clipboard_manager.lock();
manager.get_item(&id).cloned()
};
if let Some(item) = item {
// Small delay to ensure window is hidden and previous app has focus
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
// Write to clipboard and simulate paste
let manager = state.clipboard_manager.lock();
manager
.paste_item(&item)
.map_err(|e| format!("Failed to paste: {}", e))?;
}
Ok(())
}
/// Show the clipboard window at cursor position
fn show_window_at_cursor(window: &WebviewWindow) {
use tauri::{PhysicalPosition, PhysicalSize};
// Get cursor position - fallback to center if not available
let cursor_pos = window
.cursor_position()
.unwrap_or_else(|_| PhysicalPosition::new(100.0, 100.0));
// Get monitor info for bounds checking
if let Ok(Some(monitor)) = window.current_monitor() {
let monitor_size = monitor.size();
let window_size = window.outer_size().unwrap_or(PhysicalSize::new(360, 480));
// Calculate position, keeping window within screen bounds
let mut x = cursor_pos.x as i32;
let mut y = cursor_pos.y as i32;
// Adjust if window would go off-screen
if x + window_size.width as i32 > monitor_size.width as i32 {
x = monitor_size.width as i32 - window_size.width as i32 - 10;
}
if y + window_size.height as i32 > monitor_size.height as i32 {
y = monitor_size.height as i32 - window_size.height as i32 - 10;
}
let _ = window.set_position(PhysicalPosition::new(x, y));
}
let _ = window.show();
let _ = window.set_focus();
}
/// Toggle window visibility
fn toggle_window(app: &AppHandle) {
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
} else {
show_window_at_cursor(&window);
}
}
}
/// Start clipboard monitoring in background thread
fn start_clipboard_watcher(app: AppHandle, clipboard_manager: Arc<Mutex<ClipboardManager>>) {
std::thread::spawn(move || {
let mut last_text: Option<String> = None;
let mut last_image_hash: Option<u64> = None;
loop {
std::thread::sleep(std::time::Duration::from_millis(500));
let mut manager = clipboard_manager.lock();
// Check for text changes
if let Ok(text) = manager.get_current_text() {
if Some(&text) != last_text.as_ref() && !text.is_empty() {
last_text = Some(text.clone());
if let Some(item) = manager.add_text(text) {
// Emit event to frontend
let _ = app.emit("clipboard-changed", &item);
}
}
}
// Check for image changes
if let Ok(Some((image_data, hash))) = manager.get_current_image() {
if Some(hash) != last_image_hash {
last_image_hash = Some(hash);
if let Some(item) = manager.add_image(image_data) {
let _ = app.emit("clipboard-changed", &item);
}
}
}
}
});
}
/// Start global hotkey listener
fn start_hotkey_listener(app: AppHandle) -> HotkeyManager {
let app_clone = app.clone();
HotkeyManager::new(move || {
toggle_window(&app_clone);
})
}
fn main() {
let clipboard_manager = Arc::new(Mutex::new(ClipboardManager::new()));
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.manage(AppState {
clipboard_manager: clipboard_manager.clone(),
hotkey_manager: Arc::new(Mutex::new(None)),
})
.setup(move |app| {
let app_handle = app.handle().clone();
// Setup system tray
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let show_item = MenuItem::with_id(app, "show", "Show Clipboard", true, None::<&str>)?;
let clear_item = MenuItem::with_id(app, "clear", "Clear History", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &clear_item, &quit_item])?;
// Load tray icon
let icon =
Image::from_bytes(include_bytes!("../icons/icon.png")).unwrap_or_else(|_| {
Image::from_bytes(include_bytes!("../icons/32x32.png")).unwrap()
});
let _tray = TrayIconBuilder::new()
.icon(icon)
.menu(&menu)
.tooltip("Clipboard History (Super+V)")
.on_menu_event(move |app, event| match event.id.as_ref() {
"quit" => {
app.exit(0);
}
"show" => {
toggle_window(app);
}
"clear" => {
if let Some(state) = app.try_state::<AppState>() {
state.clipboard_manager.lock().clear();
let _ = app.emit("history-cleared", ());
}
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
toggle_window(tray.app_handle());
}
})
.build(app)?;
// Setup window blur handler (close on focus loss)
let main_window = app.get_webview_window("main").unwrap();
let window_clone = main_window.clone();
main_window.on_window_event(move |event| {
if let tauri::WindowEvent::Focused(false) = event {
let _ = window_clone.hide();
}
});
// Start clipboard watcher
start_clipboard_watcher(app_handle.clone(), clipboard_manager.clone());
// Start global hotkey listener
let hotkey_manager = start_hotkey_listener(app_handle.clone());
// Store hotkey manager in state
if let Some(state) = app_handle.try_state::<AppState>() {
*state.hotkey_manager.lock() = Some(hotkey_manager);
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_history,
clear_history,
delete_item,
toggle_pin,
paste_item,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

51
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,51 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "win11-clipboard-history",
"version": "1.0.0",
"identifier": "com.clipboard.win11history",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"trayIcon": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"windows": [
{
"title": "Clipboard History",
"label": "main",
"width": 360,
"height": 480,
"resizable": false,
"decorations": false,
"transparent": true,
"visible": false,
"skipTaskbar": true,
"alwaysOnTop": true,
"focus": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["deb", "appimage", "rpm"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"category": "Utility",
"shortDescription": "Windows 11-style Clipboard History Manager",
"longDescription": "A modern, beautiful clipboard history manager for Linux that mimics the Windows 11 Win+V clipboard experience. Built with Tauri, React, and Rust."
}
}

117
src/App.tsx Normal file
View File

@@ -0,0 +1,117 @@
import { useState, useCallback } from 'react'
import { clsx } from 'clsx'
import { useClipboardHistory } from './hooks/useClipboardHistory'
import { useDarkMode } from './hooks/useDarkMode'
import { HistoryItem } from './components/HistoryItem'
import { TabBar } from './components/TabBar'
import { Header } from './components/Header'
import { EmptyState } from './components/EmptyState'
import type { ActiveTab } from './types/clipboard'
/**
* Main App Component - Windows 11 Clipboard History Manager
*/
function App() {
const [activeTab, setActiveTab] = useState<ActiveTab>('clipboard')
const isDark = useDarkMode()
const { history, isLoading, clearHistory, deleteItem, togglePin, pasteItem } =
useClipboardHistory()
// Handle tab change
const handleTabChange = useCallback((tab: ActiveTab) => {
setActiveTab(tab)
}, [])
// Render content based on active tab
const renderContent = () => {
switch (activeTab) {
case 'clipboard':
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<div className="w-6 h-6 border-2 border-win11-bg-accent border-t-transparent rounded-full animate-spin" />
</div>
)
}
if (history.length === 0) {
return <EmptyState />
}
return (
<div className="flex flex-col gap-2 p-3">
{history.map((item, index) => (
<HistoryItem
key={item.id}
item={item}
index={index}
onPaste={pasteItem}
onDelete={deleteItem}
onTogglePin={togglePin}
/>
))}
</div>
)
case 'gifs':
return (
<div className="flex flex-col items-center justify-center h-full py-12 px-4 text-center">
<p className="text-sm dark:text-win11-text-secondary text-win11Light-text-secondary">
GIF search coming soon! 🎬
</p>
</div>
)
case 'emoji':
return (
<div className="flex flex-col items-center justify-center h-full py-12 px-4 text-center">
<p className="text-sm dark:text-win11-text-secondary text-win11Light-text-secondary">
Emoji picker coming soon! 😊
</p>
</div>
)
default:
return null
}
}
return (
<div
className={clsx(
// Container styles
'h-screen w-screen overflow-hidden flex flex-col',
'rounded-win11-lg',
// Glassmorphism effect
isDark ? 'glass-effect' : 'glass-effect-light',
// Background fallback
isDark ? 'bg-win11-acrylic-bg' : 'bg-win11Light-acrylic-bg',
// Text color based on theme
isDark ? 'text-win11-text-primary' : 'text-win11Light-text-primary'
)}
>
{/* Header with title and actions */}
<Header onClearHistory={clearHistory} itemCount={history.filter((i) => !i.pinned).length} />
{/* Tab bar */}
<TabBar activeTab={activeTab} onTabChange={handleTabChange} />
{/* Scrollable content area */}
<div className="flex-1 overflow-y-auto scrollbar-win11">{renderContent()}</div>
{/* Footer hint */}
<div className="px-4 py-2 text-center border-t dark:border-win11-border-subtle border-win11Light-border">
<p className="text-xs dark:text-win11-text-tertiary text-win11Light-text-secondary">
Click an item to paste Press{' '}
<kbd className="px-1 py-0.5 rounded dark:bg-win11-bg-tertiary bg-win11Light-bg-tertiary font-mono">
Esc
</kbd>{' '}
to close
</p>
</div>
</div>
)
}
export default App

View File

@@ -0,0 +1,26 @@
import { ClipboardList } from 'lucide-react'
/**
* Empty state component when there's no clipboard history
*/
export function EmptyState() {
return (
<div className="flex flex-col items-center justify-center h-full py-12 px-4 text-center">
<div className="w-16 h-16 rounded-full dark:bg-win11-bg-tertiary bg-win11Light-bg-tertiary flex items-center justify-center mb-4">
<ClipboardList className="w-8 h-8 dark:text-win11-text-tertiary text-win11Light-text-secondary" />
</div>
<h3 className="text-base font-medium dark:text-win11-text-primary text-win11Light-text-primary mb-2">
No clipboard history yet
</h3>
<p className="text-sm dark:text-win11-text-secondary text-win11Light-text-secondary max-w-[200px]">
Copy something to see it appear here. Press{' '}
<kbd className="px-1.5 py-0.5 rounded dark:bg-win11-bg-tertiary bg-win11Light-bg-tertiary text-xs font-mono">
Super+V
</kbd>{' '}
to open anytime.
</p>
</div>
)
}

56
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { Trash2, Settings } from 'lucide-react'
import { clsx } from 'clsx'
interface HeaderProps {
onClearHistory: () => void
itemCount: number
}
/**
* Header component with title and action buttons
*/
export function Header({ onClearHistory, itemCount }: HeaderProps) {
return (
<div className="flex items-center justify-between px-4 py-3" data-tauri-drag-region>
<div className="flex items-center gap-2">
<h1 className="text-sm font-semibold dark:text-win11-text-primary text-win11Light-text-primary">
Clipboard History
</h1>
{itemCount > 0 && (
<span className="text-xs px-2 py-0.5 rounded-full dark:bg-win11-bg-tertiary bg-win11Light-bg-tertiary dark:text-win11-text-secondary text-win11Light-text-secondary">
{itemCount}
</span>
)}
</div>
<div className="flex items-center gap-1">
{/* Clear history button */}
<button
onClick={onClearHistory}
disabled={itemCount === 0}
className={clsx(
'p-2 rounded-md transition-colors',
'dark:text-win11-text-secondary text-win11Light-text-secondary',
'hover:dark:bg-win11-bg-tertiary hover:bg-win11Light-bg-tertiary',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
title="Clear all"
>
<Trash2 className="w-4 h-4" />
</button>
{/* Settings button (placeholder for future) */}
<button
className={clsx(
'p-2 rounded-md transition-colors',
'dark:text-win11-text-secondary text-win11Light-text-secondary',
'hover:dark:bg-win11-bg-tertiary hover:bg-win11Light-bg-tertiary'
)}
title="Settings"
>
<Settings className="w-4 h-4" />
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,171 @@
import { useCallback } from 'react'
import { clsx } from 'clsx'
import { Pin, X, Image as ImageIcon, Type } from 'lucide-react'
import type { ClipboardItem } from '../types/clipboard'
interface HistoryItemProps {
item: ClipboardItem
onPaste: (id: string) => void
onDelete: (id: string) => void
onTogglePin: (id: string) => void
index: number
}
/**
* A single clipboard history card with Windows 11 styling
*/
export function HistoryItem({ item, onPaste, onDelete, onTogglePin, index }: HistoryItemProps) {
const isText = item.content.type === 'Text'
// Format timestamp
const formatTime = useCallback((timestamp: string) => {
const date = new Date(timestamp)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
if (diffMins < 1) return 'Just now'
if (diffMins < 60) return `${diffMins}m ago`
if (diffHours < 24) return `${diffHours}h ago`
return date.toLocaleDateString()
}, [])
// Handle paste on click
const handleClick = useCallback(() => {
onPaste(item.id)
}, [item.id, onPaste])
// Handle delete with stopPropagation
const handleDelete = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
onDelete(item.id)
},
[item.id, onDelete]
)
// Handle pin toggle with stopPropagation
const handleTogglePin = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
onTogglePin(item.id)
},
[item.id, onTogglePin]
)
return (
<div
className={clsx(
// Base styles
'group relative rounded-win11 p-3 cursor-pointer',
'transition-all duration-150 ease-out',
// Animation delay based on index
'animate-in',
// Dark mode styles
'dark:bg-win11-bg-card dark:hover:bg-win11-bg-card-hover',
'dark:border dark:border-win11-border-subtle',
// Light mode styles
'bg-win11Light-bg-card hover:bg-win11Light-bg-card-hover',
'border border-win11Light-border',
// Pinned indicator
item.pinned && 'ring-1 ring-win11-bg-accent'
)}
onClick={handleClick}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleClick()
}
}}
style={{ animationDelay: `${index * 30}ms` }}
>
{/* Content type indicator */}
<div className="flex items-start gap-3">
{/* Icon */}
<div
className={clsx(
'flex-shrink-0 w-8 h-8 rounded-md flex items-center justify-center',
'dark:bg-win11-bg-tertiary bg-win11Light-bg-tertiary'
)}
>
{isText ? (
<Type className="w-4 h-4 dark:text-win11-text-secondary text-win11Light-text-secondary" />
) : (
<ImageIcon className="w-4 h-4 dark:text-win11-text-secondary text-win11Light-text-secondary" />
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
{item.content.type === 'Text' && (
<p className="text-sm dark:text-win11-text-primary text-win11Light-text-primary line-clamp-3 break-words whitespace-pre-wrap">
{item.content.data}
</p>
)}
{item.content.type === 'Image' && (
<div className="relative">
<img
src={`data:image/png;base64,${item.content.data.base64}`}
alt="Clipboard image"
className="max-w-full max-h-24 rounded object-contain bg-black/10"
/>
<span className="absolute bottom-1 right-1 text-xs px-1.5 py-0.5 rounded bg-black/60 text-white">
{item.content.data.width}×{item.content.data.height}
</span>
</div>
)}
{/* Timestamp */}
<span className="text-xs dark:text-win11-text-tertiary text-win11Light-text-secondary mt-1 block">
{formatTime(item.timestamp)}
</span>
</div>
{/* Action buttons - visible on hover */}
<div
className={clsx(
'flex items-center gap-1 opacity-0 group-hover:opacity-100',
'transition-opacity duration-150'
)}
>
{/* Pin button */}
<button
onClick={handleTogglePin}
className={clsx(
'p-1.5 rounded-md transition-colors',
'hover:dark:bg-win11-bg-tertiary hover:bg-win11Light-bg-tertiary',
item.pinned
? 'text-win11-bg-accent'
: 'dark:text-win11-text-tertiary text-win11Light-text-secondary'
)}
title={item.pinned ? 'Unpin' : 'Pin'}
>
<Pin className="w-4 h-4" fill={item.pinned ? 'currentColor' : 'none'} />
</button>
{/* Delete button */}
<button
onClick={handleDelete}
className={clsx(
'p-1.5 rounded-md transition-colors',
'dark:text-win11-text-tertiary text-win11Light-text-secondary',
'hover:text-win11-error hover:dark:bg-win11-bg-tertiary hover:bg-win11Light-bg-tertiary'
)}
title="Delete"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
{/* Pinned badge */}
{item.pinned && (
<div className="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-win11-bg-accent" />
)}
</div>
)
}

52
src/components/TabBar.tsx Normal file
View File

@@ -0,0 +1,52 @@
import { clsx } from 'clsx'
import { ClipboardList, Smile, Image } from 'lucide-react'
import type { ActiveTab } from '../types/clipboard'
interface TabBarProps {
activeTab: ActiveTab
onTabChange: (tab: ActiveTab) => void
}
/**
* Windows 11 style tab bar for switching between Clipboard, GIFs, and Emoji
*/
export function TabBar({ activeTab, onTabChange }: TabBarProps) {
const tabs: { id: ActiveTab; label: string; icon: typeof ClipboardList }[] = [
{ id: 'clipboard', label: 'Clipboard', icon: ClipboardList },
{ id: 'gifs', label: 'GIFs', icon: Image },
{ id: 'emoji', label: 'Emoji', icon: Smile },
]
return (
<div className="flex items-center gap-1 p-2 border-b dark:border-win11-border-subtle border-win11Light-border">
{tabs.map((tab) => {
const Icon = tab.icon
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={clsx(
'flex items-center justify-center gap-2 px-4 py-2 rounded-md',
'text-sm font-medium transition-all duration-150',
'focus:outline-none focus-visible:ring-2 focus-visible:ring-win11-bg-accent',
isActive
? [
'dark:bg-win11-bg-tertiary bg-win11Light-bg-tertiary',
'dark:text-win11-text-primary text-win11Light-text-primary',
]
: [
'dark:text-win11-text-secondary text-win11Light-text-secondary',
'hover:dark:bg-win11-bg-card-hover hover:bg-win11Light-bg-card-hover',
]
)}
>
<Icon className="w-4 h-4" />
<span className="hidden sm:inline">{tab.label}</span>
</button>
)
})}
</div>
)
}

View File

@@ -0,0 +1,109 @@
import { useState, useEffect, useCallback } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { listen, UnlistenFn } from '@tauri-apps/api/event'
import type { ClipboardItem } from '../types/clipboard'
/**
* Hook for managing clipboard history
*/
export function useClipboardHistory() {
const [history, setHistory] = useState<ClipboardItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Fetch initial history
const fetchHistory = useCallback(async () => {
try {
setIsLoading(true)
const items = await invoke<ClipboardItem[]>('get_history')
setHistory(items)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch history')
} finally {
setIsLoading(false)
}
}, [])
// Clear all history
const clearHistory = useCallback(async () => {
try {
await invoke('clear_history')
setHistory((prev) => prev.filter((item) => item.pinned))
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to clear history')
}
}, [])
// Delete a specific item
const deleteItem = useCallback(async (id: string) => {
try {
await invoke('delete_item', { id })
setHistory((prev) => prev.filter((item) => item.id !== id))
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete item')
}
}, [])
// Toggle pin status
const togglePin = useCallback(async (id: string) => {
try {
const updatedItem = await invoke<ClipboardItem>('toggle_pin', { id })
if (updatedItem) {
setHistory((prev) => prev.map((item) => (item.id === id ? updatedItem : item)))
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to toggle pin')
}
}, [])
// Paste an item
const pasteItem = useCallback(async (id: string) => {
try {
await invoke('paste_item', { id })
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to paste item')
}
}, [])
// Listen for clipboard changes
useEffect(() => {
fetchHistory()
let unlistenChanged: UnlistenFn | undefined
let unlistenCleared: UnlistenFn | undefined
const setupListeners = async () => {
unlistenChanged = await listen<ClipboardItem>('clipboard-changed', (event) => {
setHistory((prev) => {
// Add new item at the top (after pinned items)
const pinnedItems = prev.filter((i) => i.pinned)
const unpinnedItems = prev.filter((i) => !i.pinned)
return [...pinnedItems, event.payload, ...unpinnedItems.slice(0, 49)]
})
})
unlistenCleared = await listen('history-cleared', () => {
setHistory((prev) => prev.filter((item) => item.pinned))
})
}
setupListeners()
return () => {
unlistenChanged?.()
unlistenCleared?.()
}
}, [fetchHistory])
return {
history,
isLoading,
error,
fetchHistory,
clearHistory,
deleteItem,
togglePin,
pasteItem,
}
}

30
src/hooks/useDarkMode.ts Normal file
View File

@@ -0,0 +1,30 @@
import { useState, useEffect } from 'react'
/**
* Hook for detecting system dark mode preference
*/
export function useDarkMode(): boolean {
const [isDark, setIsDark] = useState(() => {
if (typeof window !== 'undefined') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
return true // Default to dark mode
})
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e: MediaQueryListEvent) => {
setIsDark(e.matches)
}
// Modern browsers
mediaQuery.addEventListener('change', handleChange)
return () => {
mediaQuery.removeEventListener('change', handleChange)
}
}, [])
return isDark
}

154
src/index.css Normal file
View File

@@ -0,0 +1,154 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Base styles */
:root {
font-family:
'Segoe UI Variable',
'Segoe UI',
system-ui,
-apple-system,
sans-serif;
font-size: 14px;
line-height: 1.4;
font-weight: 400;
color-scheme: light dark;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Reset for Tauri */
html,
body,
#root {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
/* Transparent background for Tauri window */
body {
background: transparent;
}
/* Make the app draggable from certain areas */
[data-tauri-drag-region] {
-webkit-app-region: drag;
}
/* Disable text selection */
.no-select {
-webkit-user-select: none;
user-select: none;
}
/* Custom scrollbar for Windows 11 style */
@layer components {
.scrollbar-win11 {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.scrollbar-win11::-webkit-scrollbar {
width: 6px;
}
.scrollbar-win11::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-win11::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.scrollbar-win11::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.35);
}
/* Light mode scrollbar */
@media (prefers-color-scheme: light) {
.scrollbar-win11 {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
.scrollbar-win11::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
}
.scrollbar-win11::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.35);
}
}
}
/* Glassmorphism / Acrylic effect */
@layer components {
.glass-effect {
@apply backdrop-blur-acrylic;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.25),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.glass-effect-light {
@apply backdrop-blur-acrylic;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.85) 0%,
rgba(255, 255, 255, 0.75) 100%
);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.15),
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}
/* Card hover effect */
.card-hover {
@apply transition-all duration-150 ease-out;
}
.card-hover:hover {
@apply scale-[1.01];
}
.card-hover:active {
@apply scale-[0.99];
}
}
/* Animations */
@layer utilities {
.animate-in {
animation: fadeSlideIn 0.2s ease-out forwards;
}
@keyframes fadeSlideIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
/* Focus styles for accessibility */
@layer components {
.focus-ring {
@apply outline-none ring-2 ring-win11-bg-accent ring-offset-2 ring-offset-transparent;
}
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

36
src/types/clipboard.ts Normal file
View File

@@ -0,0 +1,36 @@
/** Clipboard content types */
export type ClipboardContentType = 'text' | 'image'
/** Text content */
export interface TextContent {
type: 'Text'
data: string
}
/** Image content */
export interface ImageContent {
type: 'Image'
data: {
base64: string
width: number
height: number
}
}
/** Union of all content types */
export type ClipboardContent = TextContent | ImageContent
/** A single clipboard history item */
export interface ClipboardItem {
id: string
content: ClipboardContent
timestamp: string
pinned: boolean
preview: string
}
/** Active tab in the UI */
export type ActiveTab = 'clipboard' | 'gifs' | 'emoji'
/** Theme mode */
export type ThemeMode = 'light' | 'dark' | 'system'

9
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

92
tailwind.config.js Normal file
View File

@@ -0,0 +1,92 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'media', // Use system preference
theme: {
extend: {
colors: {
// Windows 11 Dark Theme Colors
win11: {
// Backgrounds
'bg-primary': '#202020',
'bg-secondary': '#2d2d2d',
'bg-tertiary': '#383838',
'bg-card': '#2d2d2d',
'bg-card-hover': '#3d3d3d',
'bg-accent': '#0078d4',
'bg-accent-hover': '#1a86d9',
// Text
'text-primary': '#ffffff',
'text-secondary': '#c5c5c5',
'text-tertiary': '#9e9e9e',
'text-disabled': '#6e6e6e',
// Borders
'border': '#454545',
'border-subtle': '#3a3a3a',
// Acrylic/Mica effect
'acrylic-bg': 'rgba(32, 32, 32, 0.7)',
'acrylic-tint': 'rgba(255, 255, 255, 0.03)',
// Semantic colors
'success': '#6ccb5f',
'warning': '#fcb900',
'error': '#ff5f5f',
'info': '#0078d4',
},
// Light mode Windows 11 colors
win11Light: {
'bg-primary': '#f3f3f3',
'bg-secondary': '#ffffff',
'bg-tertiary': '#e5e5e5',
'bg-card': '#ffffff',
'bg-card-hover': '#f5f5f5',
'text-primary': '#1a1a1a',
'text-secondary': '#5c5c5c',
'border': '#e5e5e5',
'acrylic-bg': 'rgba(243, 243, 243, 0.7)',
}
},
borderRadius: {
'win11': '8px',
'win11-lg': '12px',
},
boxShadow: {
'win11': '0 8px 32px rgba(0, 0, 0, 0.25)',
'win11-card': '0 2px 8px rgba(0, 0, 0, 0.15)',
'win11-elevated': '0 16px 48px rgba(0, 0, 0, 0.35)',
},
backdropBlur: {
'acrylic': '20px',
},
animation: {
'fade-in': 'fadeIn 0.15s ease-out',
'slide-up': 'slideUp 0.2s ease-out',
'scale-in': 'scaleIn 0.15s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(8px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
},
fontFamily: {
'segoe': ['"Segoe UI Variable"', '"Segoe UI"', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}

31
tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
tsconfig.node.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

32
vite.config.ts Normal file
View File

@@ -0,0 +1,32 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
// Tauri expects a fixed port for development
server: {
port: 1420,
strictPort: true,
watch: {
// Workaround for WSL on Windows
usePolling: true,
},
},
// Clear screen during dev
clearScreen: false,
// Environment variables prefix
envPrefix: ['VITE_', 'TAURI_'],
build: {
// Tauri uses Chromium on Windows and WebKit on macOS and Linux
target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
// Don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// Produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
},
})