Skip to content

Repository files navigation

Jotter

A modern, cross-platform desktop text editor built with Tauri 2 + React + TypeScript. I wanted a lightweight, Notepad-style editor that also featured a rich Markdown WYSIWYG editor (via Milkdown Crepe) and multi-tab support. I couldn't find a free tool that did both well, so I built my own and open-sourced it.


Screenshots

After first launch you will see a blank Markdown tab. Open any .md file to get the full WYSIWYG experience.

screenshot


Feature Overview

Windows Notepad Parity

Feature Status
File → New Tab
File → New Window
File → Open / Save / Save As
File → Print / Page Setup ⚠️ (delegates to OS print dialog, WIP)
Edit → Undo / Redo
Edit → Cut / Copy / Paste / Delete
Edit → Find / Replace
Edit → Go To Line
Edit → Select All
Edit → Time/Date (F5)
View → Zoom In / Out / Reset ✅ (via View menu for now)
View → Word Wrap toggle
View → Status Bar toggle
Unsaved-changes warning on close

Modern Upgrades

Feature Details
Multi-tab editing Drag-and-drop reorder, middle-click close, Ctrl+T / Ctrl+W
Markdown WYSIWYG Rich editor via Milkdown Crepe with slash commands, toolbar, tables, code blocks, images
Per-tab word wrap Each tab remembers its own word-wrap state
Global settings Theme, font size, word wrap default
Persistent settings Saved to disk via tauri-plugin-store, survives restarts
Themes System / Light / Dark — reactive, no restart needed
DOCX Export/Import Export Markdown to Word (.docx) with embedded images, import Word to Markdown
Recently Opened File menu tracks recently opened files
"Open With" Right-click any .txt or .md file → open in Jotter
Single-instance Opening a file while the app is running focuses the existing window
Deep links notepadpro:// scheme for file associations
Status bar Live line/col count, line count, char count, word-wrap indicator

Tech Stack

Layer Technology
Desktop runtime Tauri 2 (Rust)
Frontend framework React 18 + TypeScript
Build tool Vite 6
Styling Tailwind CSS v4 + CSS custom properties
UI primitives Radix UI
State management Zustand 5 + Immer
Settings persistence tauri-plugin-store
Markdown editor Milkdown + Crepe preset
Tab drag-and-drop @dnd-kit/core + @dnd-kit/sortable
DOCX export/import docx (export) + mammoth + turndown (import)
Icons Lucide React

📥 Download

Download the latest compiled version for your platform from the Releases page.

  • Windows: .exe or .msi installer
  • macOS: .dmg (Universal)
  • Linux: .AppImage, .deb, or .rpm

Prerequisites

You need these installed before anything else.

1. Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

2. Node.js 20+

# Using nvm (recommended)
nvm install 20 && nvm use 20

# Or download from https://nodejs.org

3. Linux only — system dependencies

sudo apt-get update
sudo apt-get install -y \
  libwebkit2gtk-4.1-dev \
  libappindicator3-dev \
  librsvg2-dev \
  patchelf \
  libxdo-dev \
  libssl-dev \
  pkg-config

4. macOS only

Xcode Command Line Tools are required:

xcode-select --install

Important: macOS builds cannot be cross-compiled from Windows or Linux. You must either develop on a Mac or use the provided GitHub Actions workflow (which uses Apple's hosted runners).


Getting Started

Clone & Install

git clone https://github.com/POWERHACK69/Jotter.git
cd Jotter

# Install JavaScript dependencies
npm install

Generate App Icons

Replace the placeholder icons with your own 1024×1024 PNG, then run:

npx tauri icon path/to/your-icon.png

This auto-generates all required sizes for Windows (.ico), macOS (.icns), and Linux (.png).

Development

npm run tauri dev

This starts Vite's dev server on port 1420 and launches the Tauri window. Hot-module replacement works for all React components. Rust changes require a restart.

Production Build

npm run tauri build

Outputs are placed in src-tauri/target/release/bundle/:

Platform Format Location
Windows .msi + .exe (NSIS) bundle/msi/, bundle/nsis/
macOS .dmg + .app bundle/dmg/, bundle/macos/
Linux .deb + .AppImage + .rpm bundle/deb/, bundle/appimage/, bundle/rpm/

Project Structure

Jotter/
├── .github/
│   └── workflows/
│       └── release.yml          # Cross-platform CI/CD (Win + Linux + macOS arm64 + x64)
│
├── src/                         # React / TypeScript frontend
│   ├── components/
│   │   ├── editor/
│   │   │   ├── EditorPane.tsx       # Orchestrator: renders active editor
│   │   │   ├── MarkdownEditor.tsx   # Milkdown + Crepe WYSIWYG
│   │   │   └── EmptyState.tsx       # No-tabs-open screen
│   │   ├── menu/
│   │   │   ├── MenuBar.tsx          # Root + shared primitives
│   │   │   ├── FileMenu.tsx
│   │   │   ├── EditMenu.tsx
│   │   │   └── ViewMenu.tsx
│   │   ├── tabs/
│   │   │   └── TabBar.tsx           # DnD sortable tab bar
│   │   ├── dialogs/
│   │   │   ├── FindReplaceDialog.tsx # Floating find/replace panel
│   │   │   ├── GoToDialog.tsx        # Go To Line modal
│   │   │   ├── SettingsModal.tsx     # Global preferences modal
│   │   │   └── AboutDialog.tsx       # About & help dialog
│   │   └── StatusBar.tsx
│   │
│   ├── hooks/
│   │   ├── useAutoUpdater.ts        # Checks for app updates on startup (stub)
│   │   ├── useDarkMode.ts           # MutationObserver on <html> class
│   │   ├── useFileOperations.ts     # All file I/O — open, save, close
│   │   ├── useKeyboardShortcuts.ts  # Global hotkey registration
│   │   ├── useOpenFileEvents.ts     # "Open With" while app is running
│   │   ├── useRecentFiles.ts        # Recently Opened files list (persisted)
│   │   ├── useTheme.ts              # Applies theme to <html>
│   │   └── useWindowCloseGuard.ts   # Intercepts OS close → dirty tab check
│   │
│   ├── store/
│   │   ├── useAppStore.ts           # Zustand store (tabs + settings)
│   │   └── settingsStore.ts         # tauri-plugin-store bridge
│   │
│   ├── styles/
│   │   └── milkdown.css             # Crepe theme override (CSS variables)
│   │
│   ├── types/
│   │   └── index.ts                 # Tab, AppSettings, EditorFeatures types
│   │
│   ├── lib/
│   │   ├── utils.ts                 # cn(), fileNameFromPath(), isMarkdownFile()
│   │   ├── docxUtils.ts             # DOCX export/import (markdown ↔ .docx)
│   │   └── gdscript.ts              # GDScript syntax for code blocks
│   │
│   ├── App.tsx                      # Root component + startup sequence
│   ├── main.tsx                     # React entry point
│   └── index.css                    # Tailwind v4 + CSS custom properties
│
├── src-tauri/                   # Rust / Tauri backend
│   ├── src/
│   │   ├── main.rs                  # Builder + plugin registration
│   │   ├── commands.rs              # Tauri commands (CLI args, file I/O, reveal in folder)
│   │   ├── file_associations.rs     # macOS "Open With" via deep-link plugin
│   │   └── single_instance.rs      # Single-instance: forward args to existing window
│   ├── entitlements.plist           # macOS sandbox entitlements
│   ├── Info.plist                   # macOS file type + URL scheme registration
│   ├── Cargo.toml
│   ├── build.rs
│   └── tauri.conf.json              # App config, bundle targets, file associations
│
├── index.html
├── vite.config.ts
├── tsconfig.json
└── package.json

Keyboard Shortcuts

Shortcut Action
Ctrl+T New tab
Ctrl+O Open file
Ctrl+S Save
Ctrl+Shift+S Save As
Ctrl+W Close active tab
Ctrl+Z Undo
Ctrl+Y Redo
Ctrl+X / C / V Cut / Copy / Paste
Ctrl+A Select all
Ctrl+F Find
Ctrl+R Replace
F5 Insert current time/date

The following shortcuts are available via the menu bar:

  • Ctrl+N — New Window (File menu) ❌
  • Ctrl+G — Go to Line (Edit menu) ⚠️
  • Ctrl+, — Open Settings (File menu)
  • Ctrl+= / Ctrl+- — Zoom in / out (View menu) ❌
  • Ctrl+0 — Reset zoom (View menu) ❌

Architecture Notes

Tab Isolation

Each tab carries its own complete state: filePath, content, isDirty, wordWrap. The key={activeTab.id} prop on the editor component forces React to fully unmount and remount the editor when switching tabs — this prevents stale internal state from leaking between documents.

Paste Sync (Milkdown)

Milkdown's markdownUpdated listener does not reliably fire for paste events. When a user pastes content without making another edit, the Zustand store never receives the updated content, and switching tabs (which triggers a full remount) would lose the pasted text. To work around this, the editor attaches a paste event listener on the ProseMirror element that force-syncs the full markdown to the store after a setTimeout(fn, 0) delay — long enough for Milkdown's internal paste processing to complete.

State Flow

Disk (tauri-plugin-store)
    ↕  loadSettingsFromDisk / saveSettingsToDisk
Zustand (useAppStore)
    ↕  props / hooks
React Components
    ↕  onChange callbacks
Zustand (setTabContent)
    ↕  writeTextFile (on Save)
Disk (fs)

Window Close Guard

The Rust backend calls api.prevent_close() on every close attempt and emits a close-requested event. The React useWindowCloseGuard hook intercepts this, iterates over all dirty tabs one by one (activating each so the user sees which file they're deciding about), shows a native save dialog, and only calls exit(0) once all tabs are resolved. If the user cancels at any point, the close is aborted entirely.


Configuration

Settings (persisted across restarts)

Open File → Settings or press Ctrl+,:

Setting Default Description
Theme System System Default / Light / Dark
Font Size 15px Editor font size
Word Wrap by Default On Default for new tabs

Settings are stored in your OS app data directory:

Platform Path
Windows %APPDATA%\com.jotter.app\jotter.settings.json
macOS ~/Library/Application Support/com.jotter.app/jotter.settings.json
Linux ~/.local/share/com.jotter.app/jotter.settings.json

Releasing

Automated (GitHub Actions)

  1. Push a tag: git tag v0.1.0 && git push --tags
  2. The workflow builds for all four targets (Linux x64, Windows x64, macOS arm64, macOS x64) and creates a draft GitHub Release with all installers attached.
  3. Review the draft, add release notes, and publish.

Code Signing (optional but recommended)

macOS

Add these secrets to your GitHub repository:

Secret Description
APPLE_CERTIFICATE Base64-encoded .p12 certificate
APPLE_CERTIFICATE_PASSWORD Password for the .p12
APPLE_SIGNING_IDENTITY e.g. Developer ID Application: Your Name (TEAMID)
APPLE_ID Your Apple ID email
APPLE_PASSWORD App-specific password from appleid.apple.com
APPLE_TEAM_ID Your 10-character team ID

Then uncomment the corresponding lines in .github/workflows/release.yml.

Windows

Generate a self-signed or purchased code signing certificate, then add:

Secret Description
TAURI_SIGNING_PRIVATE_KEY Base64-encoded private key
TAURI_SIGNING_PRIVATE_KEY_PASSWORD Key password

Auto-updater

To enable in-app update checks, add to tauri.conf.json under plugins:

"updater": {
  "endpoints": ["https://your-update-server.com/{{target}}/{{arch}}/{{current_version}}"],
  "pubkey": "your-public-key-here"
}

See the Tauri updater docs for server setup.


"Open With" Integration

After installing the app:

Windows

File associations for .txt, .md, .log are registered automatically by the NSIS/MSI installer. Right-click any file → "Open with" → "Jotter".

macOS

The app appears in Finder's "Open With" menu for .txt, .md, .markdown, .log files automatically via Info.plist and CFBundleDocumentTypes.

Linux

Register manually after installing the .deb or .AppImage:

# If using the .deb installer, this is done automatically.
# For AppImage, register manually:
xdg-mime default jotter.desktop text/plain
xdg-mime default jotter.desktop text/markdown

Deep Links (all platforms)

Open a file directly from a URL:

jotter://open?path=/absolute/path/to/file.txt

Note: The deep link scheme is configured as jotter:// in tauri.conf.json. The exact URL format depends on your OS file association setup.


Customisation

Changing the Accent Colour

Edit the --accent CSS custom property in src/index.css:

:root {
  --accent: 0 120 212; /* R G B — Windows blue by default */
}

Extending File Associations

Add extensions to the fileAssociations array in src-tauri/tauri.conf.json and to the open dialog filters in src/hooks/useFileOperations.ts.


Known Limitations

Limitation Notes
Print / Page Setup Delegates to the browser's native print dialog. A dedicated print preview with margin control is not yet implemented.
Auto-updater The scaffold is in place (useAutoUpdater.ts) but requires a distribution server and signing keys to activate.
macOS cross-compilation Not possible from Windows/Linux. Must use macOS hardware or the provided GitHub Actions workflow.

Development Tips

Hot Reload

Vite HMR works for all frontend changes. For Rust changes, the Tauri process restarts automatically thanks to watch = { ignored = ["**/src-tauri/**"] } in vite.config.ts combined with cargo tauri dev's file watcher.

Inspecting the WebView

In dev mode, right-click anywhere in the window and choose Inspect Element (same as Chrome DevTools). In production builds this is disabled by default.

Resetting Stored Settings

Delete the settings file for your platform (paths listed in the Configuration section above), or use the Tauri Store API from the DevTools console:

// In the DevTools console during development:
window.__TAURI__.invoke('plugin:store|clear', { path: 'jotter.settings.json' })

Contributing

  1. Fork the repo
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit your changes: git commit -m 'Add my feature'
  4. Push: git push origin feature/my-feature
  5. Open a Pull Request

Please ensure:

  • TypeScript strict mode passes (npx tsc --noEmit)
  • No new console.error calls without corresponding error handling
  • New Tauri commands are added to invoke_handler! in main.rs

AI Disclosure

This project was developed with the assistance of AI coding tools for code generation, and debugging.


License

MIT — see LICENSE for details. Feel free to contribute and fork!

About

A Notepad-inspired text editor with multi-tab support, a rich Markdown WYSIWYG interface, and GDScript code blocks.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages