Skip to content

Commit 05f835a

Browse files
authored
fix(tauri): UI fixes - mic button state and window persistence (#9)
* fix(tauri): remove mic button state changes and prevent empty window opening - Remove recording button color changes (now just a link to recording window) - Simplify CSS by removing recording state classes and animations - Fix window opening without document on startup * feat(macos): add debug build with dev version display - Add BUNDLE_VERSION variable in project.yml (dev for Debug, 0.2.0 for Release) - Update Info.plist to use $(BUNDLE_VERSION) for version strings - Add 'make dev' and 'make build-debug' targets for Debug builds - About panel now shows 'Version dev' in development builds * docs: mark macOS native app as deprecated in CLAUDE.md The Tauri version is now the primary and actively maintained version. macOS native (apps/macos/) is no longer receiving updates. * fix(tauri): enable window-state plugin to persist window position The plugin was installed but not initialized. Now windows will save and restore their position/size between sessions. * fix(tauri): await loadFile to ensure window shows when opening via CLI * fix(tauri): use correct button classes in modal dialogs Replace modal-btn with btn and modal-btn-primary with btn-primary to fix styling in dark theme. * fix(tauri): exclude recording window from window-state persistence The recording window should always start hidden, not restore previous visibility state. * feat(tauri): add double-click on toolbar to toggle maximize Adds native macOS behavior - double-clicking the title bar toggles maximize/restore. * fix(tauri): adjust toolbar padding to center macOS window buttons * feat(build): add Makefile and local dev build script - Add apps/tauri/Makefile with targets: dev, build, build-dev, install, clean - Add scripts/build-dev.sh for local builds with git hash (0.0.0-{hash}) - Update CLAUDE.md with Makefile usage and build documentation - Local builds clearly show version 0.0.0-abc1234 to differentiate from releases * feat(ui): add active state tracking for outline headings - Highlight current visible heading in outline sidebar - Use IntersectionObserver to track scroll position - Auto-scroll outline to keep active heading visible - Add visual styling for active heading with border and color * docs: update README with Makefile build commands * fix(whisper): use None for automatic language detection Changed from Some("auto") to None as per whisper.cpp API specification for automatic language detection * fix(ui): keep last active heading highlighted in outline Persist last active heading when scrolling between sections to prevent outline from losing active state * fix(build): ensure ~/Applications exists before install copy * docs: include Info.plist in set-version description * fix(build): add dev target to .PHONY in macOS Makefile * fix(macos): add microphone permission description to Info.plist macOS requires NSMicrophoneUsageDescription in Info.plist for apps that access the microphone. Without it, the system silently blocks microphone access when the app is launched via Finder/Spotlight, causing Whisper transcription to return [BLANK_AUDIO]. The app worked when launched from terminal because it inherited permissions from Terminal.app. * fix(whisper): ensure audio stream is properly closed on stop Implements Drop trait for AudioRecorder to explicitly close the audio stream when the recorder is destroyed. Adds a 100ms delay after stopping the stream to give macOS time to update the microphone indicator icon in the menu bar. This prevents the microphone icon from staying active after recording is stopped or cancelled. * docs: clarify build-dev.sh must run from repo root * fix(whisper): explicitly pause audio stream before dropping Calls stream.pause() before dropping to ensure the audio device is properly released. Increases delay from 100ms to 200ms to give macOS more time to update the microphone indicator in the menu bar. Applies to both stop() method and Drop trait implementation.
1 parent 1597f67 commit 05f835a

15 files changed

Lines changed: 329 additions & 89 deletions

File tree

CLAUDE.md

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
Arandu is a Markdown viewer application with two frontends that share the same feature set:
8-
- **macOS native** (`apps/macos/`) — Swift + AppKit + WebKit, uses `libcmark_gfm` for GFM rendering
9-
- **Tauri cross-platform** (`apps/tauri/`) — Rust backend + vanilla HTML/JS frontend, uses `comrak` for GFM rendering
7+
Arandu is a Markdown viewer application built with Tauri (Rust backend + vanilla HTML/JS frontend). It uses `comrak` for GFM rendering and provides:
8+
- GitHub Flavored Markdown support (tables, task lists, strikethrough, autolinks)
9+
- Theme cycling (system/light/dark)
10+
- File watching with live reload
11+
- Sidebar outline navigation
12+
- CLI installer for macOS
13+
- Offline voice-to-text via Whisper
14+
- Plan review comment system
1015

11-
Both render GitHub Flavored Markdown (tables, task lists, strikethrough, autolinks), support theme cycling (system/light/dark), file watching with live reload, sidebar outline navigation, and include a CLI installer for macOS.
16+
**Note:** The macOS native version (`apps/macos/`) is deprecated and no longer maintained. All active development happens in the Tauri version (`apps/tauri/`).
1217

1318
## Build Commands
1419

15-
### macOS Native (requires Xcode + xcodegen)
20+
### Tauri (requires Rust + Node.js)
21+
22+
**Using Makefile (recommended):**
1623
```bash
17-
brew install xcodegen # one-time setup
18-
cd apps/macos
19-
make generate # generate .xcodeproj from project.yml
20-
make build # build Release config
21-
make install # build + install app to ~/Applications + CLI to /usr/local/bin
22-
make dist # build + create dist/Arandu.dmg
23-
make clean # remove build artifacts and .xcodeproj
24+
cd apps/tauri
25+
make dev # run in development mode (hot reload)
26+
make build # production build (uses version from tauri.conf.json)
27+
make build-dev # local dev build with git hash (e.g. 0.0.0-abc1234)
28+
make install # install app to ~/Applications + CLI to /usr/local/bin
29+
make clean # remove build artifacts
30+
make help # show all available targets
2431
```
2532

26-
### Tauri (requires Rust + Node.js)
33+
**Using npm/npx directly:**
2734
```bash
2835
cd apps/tauri
29-
npm install # install frontend dependencies
30-
npx tauri dev # run in development mode (hot reload on localhost:1420)
31-
npx tauri build # production build (outputs to src-tauri/target/release)
32-
npx tauri build --target <triple> # cross-compile (e.g. aarch64-apple-darwin)
36+
npm install # install frontend dependencies
37+
npx tauri dev # run in development mode
38+
npx tauri build # production build
39+
npx tauri build --target <triple> # cross-compile (e.g. aarch64-apple-darwin)
40+
```
41+
42+
**Local development builds:**
43+
```bash
44+
# Build with git hash as version (e.g. Arandu_0.0.0-05ca7c4_aarch64.dmg)
45+
# Run from repo root:
46+
./scripts/build-dev.sh
47+
48+
# This script temporarily updates version files, builds, then restores them
49+
# Output clearly shows it's a local dev build, not an official release
3350
```
3451

3552
### Version Management
3653
```bash
3754
scripts/set-version.sh 0.3.0 # updates Info.plist, Cargo.toml, tauri.conf.json, package.json
3855
```
3956

40-
## Architecture
57+
<details>
58+
<summary>Deprecated: macOS Native (no longer maintained)</summary>
4159

42-
### Shared Assets (`shared/`)
43-
CSS styles (`style.css`) and highlight.js files shared between both apps. The Tauri frontend has copies in `apps/tauri/src/`; the macOS app bundles them from `apps/macos/Sources/Arandu/Resources/`.
60+
```bash
61+
brew install xcodegen # one-time setup
62+
cd apps/macos
63+
make generate # generate .xcodeproj from project.yml
64+
make build # build Release config
65+
make install # build + install app to ~/Applications + CLI to /usr/local/bin
66+
make dist # build + create dist/Arandu.dmg
67+
make clean # remove build artifacts and .xcodeproj
68+
```
4469

45-
### macOS Native App (`apps/macos/`)
46-
Single-file Swift app (`Sources/Arandu/main.swift`) containing AppDelegate, MarkdownWindowController, CLIInstaller, and all UI logic. Uses `project.yml` (XcodeGen) to generate the Xcode project. Markdown rendering via C library `libcmark_gfm`. File watching uses `DispatchSource`.
70+
This version is deprecated and no longer receives updates. Use the Tauri version instead.
71+
</details>
72+
73+
## Architecture
4774

4875
### Tauri App (`apps/tauri/`)
49-
- **Rust backend** (`src-tauri/src/`): `lib.rs` defines all Tauri commands (`render_markdown`, `read_file`, `extract_headings`, `watch_file`, etc.) and app setup. `cli_installer.rs` handles macOS CLI installation (conditionally compiled with `#[cfg(target_os = "macos")]`). Markdown rendering via `comrak` crate. File watching via `notify` crate.
50-
- **JS frontend** (`src/`): `main.js` is the single entry point — communicates with Rust via `window.__TAURI__.core.invoke()`. `index.html` has the full UI including CLI installer modals. No build step or bundler; plain JS served directly.
51-
- **Tauri plugins**: `cli` (CLI arg parsing), `dialog` (file open), `fs` (file read), `updater` (auto-update from GitHub releases).
76+
- **Rust backend** (`src-tauri/src/`):
77+
- `lib.rs` defines all Tauri commands (`render_markdown`, `read_file`, `extract_headings`, `watch_file`, etc.) and app setup
78+
- `cli_installer.rs` handles macOS CLI installation (conditionally compiled with `#[cfg(target_os = "macos")]`)
79+
- `whisper.rs` handles offline voice-to-text transcription using Whisper models
80+
- `comments.rs` manages plan review comments storage
81+
- Markdown rendering via `comrak` crate, file watching via `notify` crate
82+
- **JS frontend** (`src/`):
83+
- `main.js` is the single entry point — communicates with Rust via `window.__TAURI__.core.invoke()`
84+
- `index.html` has the full UI including CLI installer modals, whisper settings, comment system
85+
- No build step or bundler; plain JS served directly
86+
- Uses shared CSS from `shared/` directory (symlinked in `src/`)
87+
- **Tauri plugins**: `cli` (CLI arg parsing), `dialog` (file open), `fs` (file read), `updater` (auto-update from GitHub releases)
88+
89+
### Shared Assets (`shared/`)
90+
CSS styles (`style.css`) and highlight.js files. The Tauri frontend symlinks these files in `apps/tauri/src/`.
5291

5392
### Website (`website/`)
5493
Static landing page deployed to Cloudflare Pages. No build step — plain HTML/CSS/JS.

README.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,23 +83,38 @@ Record audio and transcribe to text using OpenAI Whisper models (runs locally, n
8383
- [Rust](https://www.rust-lang.org/tools/install) (stable)
8484
- [Node.js](https://nodejs.org) 20+
8585

86-
### Run locally
86+
### Quick Start
8787

8888
```bash
8989
cd apps/tauri
90-
npm install
91-
npx tauri dev
90+
npm install # install dependencies
91+
make dev # run in development mode
9292
```
9393

94-
### Production build
94+
### Build Commands
95+
96+
**Using Makefile (recommended):**
97+
98+
```bash
99+
cd apps/tauri
100+
make dev # run in development mode (hot reload)
101+
make build # production build
102+
make build-dev # local dev build with git hash (e.g. Arandu_0.0.0-abc1234.dmg)
103+
make install # install app to ~/Applications + CLI to /usr/local/bin
104+
make clean # remove build artifacts
105+
make help # show all available targets
106+
```
107+
108+
**Using npm/npx directly:**
95109

96110
```bash
97111
cd apps/tauri
98-
npx tauri build
112+
npx tauri dev # development mode
113+
npx tauri build # production build
99114
```
100115

101-
### Set version across all configs
116+
### Version Management
102117

103118
```bash
104-
scripts/set-version.sh 0.3.0
119+
scripts/set-version.sh 0.3.0 # update version across all config files
105120
```

apps/macos/Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: all generate build install install-script uninstall clean dist
1+
.PHONY: all generate build build-debug dev install install-script uninstall clean dist
22

33
SCHEME = Arandu
44
CONFIG = Release
@@ -9,6 +9,8 @@ CLI_DEST = /usr/local/bin/arandu
99

1010
all: generate build
1111

12+
dev: generate build-debug
13+
1214
generate:
1315
xcodegen generate
1416

@@ -22,6 +24,16 @@ build: generate
2224
CODE_SIGNING_ALLOWED=NO \
2325
$(if $(shell which xcpretty 2>/dev/null),| xcpretty,)
2426

27+
build-debug: generate
28+
xcodebuild \
29+
-scheme $(SCHEME) \
30+
-configuration Debug \
31+
-derivedDataPath $(BUILD_DIR) \
32+
CODE_SIGN_IDENTITY="-" \
33+
CODE_SIGNING_REQUIRED=NO \
34+
CODE_SIGNING_ALLOWED=NO \
35+
$(if $(shell which xcpretty 2>/dev/null),| xcpretty,)
36+
2537
install: build
2638
@echo "Installing $(APP_NAME) to /Applications..."
2739
cp -R "$(BUILD_DIR)/Build/Products/$(CONFIG)/$(APP_NAME)" "$(APP_DEST)"

apps/macos/Sources/Arandu/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
<key>CFBundlePackageType</key>
4040
<string>APPL</string>
4141
<key>CFBundleShortVersionString</key>
42-
<string>0.2.0</string>
42+
<string>$(BUNDLE_VERSION)</string>
4343
<key>CFBundleVersion</key>
44-
<string>0.2.0</string>
44+
<string>$(BUNDLE_VERSION)</string>
4545
<key>LSMinimumSystemVersion</key>
4646
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
4747
<key>NSAppTransportSecurity</key>

apps/macos/project.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,8 @@ targets:
3434
MACOSX_DEPLOYMENT_TARGET: "13.0"
3535
ENABLE_HARDENED_RUNTIME: NO
3636
SWIFT_OBJC_BRIDGING_HEADER: ""
37+
configs:
38+
Debug:
39+
BUNDLE_VERSION: dev
40+
Release:
41+
BUNDLE_VERSION: 0.2.0

apps/tauri/Makefile

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
.PHONY: help dev build build-dev clean install
2+
3+
# Directories
4+
BUILD_DIR = src-tauri/target/release
5+
BUNDLE_DIR = $(BUILD_DIR)/bundle/macos
6+
APP_NAME = Arandu.app
7+
APP_DEST = $(HOME)/Applications/$(APP_NAME)
8+
CLI_DEST = /usr/local/bin/arandu
9+
10+
help:
11+
@echo "Arandu Tauri - Build targets:"
12+
@echo ""
13+
@echo " make dev - Run in development mode (hot reload)"
14+
@echo " make build - Production build (uses version from tauri.conf.json)"
15+
@echo " make build-dev - Local dev build with git hash (0.0.0-{hash})"
16+
@echo " make install - Install app to ~/Applications and CLI to /usr/local/bin"
17+
@echo " make clean - Remove build artifacts"
18+
@echo ""
19+
@echo "Examples:"
20+
@echo " make build-dev # Creates Arandu_0.0.0-abc1234_aarch64.dmg"
21+
@echo " make install # Installs the built app locally"
22+
23+
dev:
24+
@echo "🚀 Starting Tauri in development mode..."
25+
npm run tauri dev
26+
27+
build:
28+
@echo "📦 Building Tauri app (production)..."
29+
npx tauri build
30+
31+
build-dev:
32+
@echo "🔨 Building local dev version with git hash..."
33+
@../../scripts/build-dev.sh
34+
35+
install:
36+
@if [ ! -d "$(BUNDLE_DIR)/$(APP_NAME)" ]; then \
37+
echo "❌ App not found. Run 'make build' or 'make build-dev' first."; \
38+
exit 1; \
39+
fi
40+
@mkdir -p "$(HOME)/Applications"
41+
@echo "Installing $(APP_NAME) to ~/Applications..."
42+
@cp -R "$(BUNDLE_DIR)/$(APP_NAME)" "$(APP_DEST)"
43+
@echo "Installing CLI to $(CLI_DEST)..."
44+
@if [ -f "../../scripts/arandu" ]; then \
45+
sudo cp ../../scripts/arandu "$(CLI_DEST)"; \
46+
sudo chmod +x "$(CLI_DEST)"; \
47+
else \
48+
echo "⚠️ CLI script not found at scripts/arandu"; \
49+
fi
50+
@echo "✅ Done. Use 'arandu README.md' to open files."
51+
52+
clean:
53+
@echo "🧹 Cleaning build artifacts..."
54+
@rm -rf src-tauri/target
55+
@echo "✅ Clean complete."

apps/tauri/src-tauri/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44
<dict>
55
<key>LSUIElement</key>
66
<true/>
7+
<key>NSMicrophoneUsageDescription</key>
8+
<string>Arandu uses the microphone for voice-to-text transcription with Whisper AI.</string>
79
</dict>
810
</plist>

apps/tauri/src-tauri/capabilities/default.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"core:window:allow-show",
1111
"core:window:allow-set-focus",
1212
"core:window:allow-unminimize",
13+
"core:window:allow-toggle-maximize",
1314
"cli:default",
1415
"dialog:default",
1516
"fs:default",

apps/tauri/src-tauri/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,11 @@ pub fn run() {
341341
.plugin(tauri_plugin_updater::Builder::new().build())
342342
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
343343
.plugin(tauri_plugin_clipboard_manager::init())
344+
.plugin(
345+
tauri_plugin_window_state::Builder::default()
346+
.with_denylist(&["recording"])
347+
.build()
348+
)
344349
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
345350
eprintln!("Second instance detected: {:?}", args);
346351

apps/tauri/src-tauri/src/whisper/audio.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ fn check_microphone_permission() -> Result<bool, String> {
7676
Ok(true)
7777
}
7878

79+
impl Drop for AudioRecorder {
80+
fn drop(&mut self) {
81+
if let Some(stream) = self.stream.take() {
82+
let _ = stream.pause();
83+
drop(stream);
84+
std::thread::sleep(std::time::Duration::from_millis(200));
85+
}
86+
}
87+
}
88+
7989
impl AudioRecorder {
8090
pub fn new(device_name: Option<String>) -> Result<Self, String> {
8191
#[cfg(target_os = "macos")]
@@ -218,7 +228,11 @@ impl AudioRecorder {
218228
pub fn stop(&mut self) -> Result<Vec<f32>, String> {
219229
std::thread::sleep(std::time::Duration::from_millis(50));
220230

221-
self.stream.take();
231+
if let Some(stream) = self.stream.take() {
232+
let _ = stream.pause();
233+
drop(stream);
234+
}
235+
std::thread::sleep(std::time::Duration::from_millis(200));
222236

223237
let audio_received = {
224238
let tracker = self.last_audio_received.lock().map_err(|e| e.to_string())?;

0 commit comments

Comments
 (0)