Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ crates/
src-tauri/src/
commands.rs -- #[tauri::command] wrappers over openwith-core + apps cache
tray.rs -- tray icon lifecycle + popover positioning (plugin-positioner)
lib.rs -- tauri Builder, plugins (dialog, opener, positioner, autostart, global-shortcut ⌥⌘O)
lib.rs -- tauri Builder, plugins (dialog, opener, positioner, autostart, global-shortcut — default ⌥⌘O, user-configurable)
```

### Key patterns
Expand All @@ -93,7 +93,7 @@ crates/
- Export/import uses serde + toml crate with `BTreeMap<String, String>` for sorted, human-readable TOML; import validates apps exist and skips associations already set correctly.
- GUI: single `get_snapshot` command returns apps + associations (with sibling-UTI conflict data) + contested schemes in one call; the frontend is a plain render-to-innerHTML loop with `data-action` event delegation, no framework. Versions are lockstep: `tauri.conf.json` omits `version` so the app version comes from `workspace.package` in the root Cargo.toml.
- `openwith-core::history` is the shared change log (capped at 500 events, best-effort writes that never fail the triggering change). CLI, GUI, and core import all record into it; the GUI Profiles panel shows export/import events, the menu-bar popover shows set events with per-entry Undo, and `openwith history`/`openwith undo` read the same file.
- The GUI is two windows off one Vite bundle: `main` and a hidden transparent `menubar` popover (requires `macOSPrivateApi: true`). The popover hides on blur and is toggled by the tray icon or ⌥⌘O. A backend `AppsCache` (refreshed by `get_snapshot`) keeps popover lookups instant.
- The GUI is two windows off one Vite bundle: `main` and a hidden transparent `menubar` popover (requires `macOSPrivateApi: true`). The popover hides on blur and is toggled by the tray icon or a configurable global shortcut (default ⌥⌘O; `set_toggle_shortcut` swaps the registration at runtime, the saved accelerator is re-applied at bootstrap). A **Pin** button suspends hide-on-blur for one showing (backend `PopoverPinned` AtomicBool, reset on every toggle) so a file can be dragged in from Finder — without it the click into Finder blurs and hides the panel. Focus events are unreliable for the transparent panel, so the backend emits `popover-shown` on every open and the popover refreshes from that, not just from focus. A backend `AppsCache` (refreshed by `get_snapshot`) keeps popover lookups instant.
- GUI settings live in localStorage (`openwith.settings`). The Settings pane mirrors the design prototype's full layout; controls whose feature ships in a later 0.5.x phase (launch at login, menu bar) render disabled with an "arrives in v0.5.1" note rather than as silently-dead toggles.
- GUI visual source of truth is the claude.design prototype "OpenWith GUI Explorations" (project 14225854-984c-4e5c-8d2b-8c9ce38a1624), variants 1c (light) / 2a (dark): glyph tab icons (⌸ ⊞ ⤴ ⇅ — never emoji), 2-char initial chips (20px rows / 26px app list / 52px detail), fixed mid accent oklch(0.62 0.14 45) for tab underline + toggles, inverted toast. Check UI changes against it before shipping.

Expand Down Expand Up @@ -163,7 +163,14 @@ Run against the built .app (not just `tauri dev`) before tagging any release wit
- [ ] Appearance: flip System/Light/Dark with the popover open — both windows restyle
- [ ] Every Settings toggle: launch at login, confirm before applying, warn on UTI conflicts, show bundle IDs, relaunch Finder, check automatically, channel, open-on-tab
- [ ] Set a default from the Extensions sheet; verify with `openwith current <ext>`; Undo from the toast; verify again
- [ ] Toasts dismiss themselves (~5s, ~8s with an Undo button) without being replaced
- [ ] Popover: extension lookup, change, Recent Changes + per-entry Undo
- [ ] Make a change in the main window, then open the popover — it appears under Recent Changes
- [ ] Popover Pin: pin, click into Finder (panel must stay up), drag a file onto it — extension lookup runs; unpinned panel still hides on blur; Esc and tray-toggle reset the pin
- [ ] Rebind the popover shortcut in Settings; old combo dead, new combo toggles; survives an app relaunch; ⌥⌘O labels in Settings + popover follow
- [ ] Toggle "Warn on UTI conflicts" off — UTI ⚠ badges disappear from the Extensions table; sheet + toast warnings stay off too
- [ ] Toggle "Show bundle IDs" off — bundle IDs vanish from Extensions table, Apps detail header, and popover rows
- [ ] With the CLI upgraded via brew while the app runs: close and reopen Settings — the Command Line panel shows the new version without an app relaunch
- [ ] Profiles: export; import via choose AND drag-drop; dry-run preview; apply; dismiss
- [ ] History panel scrolls at 50 entries and updates after changes
- [ ] Check Now (updates) reports a sensible result on both channels
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/openwith-core", "crates/openwith-cli", "crates/openwith-gui/src-tauri"]

[workspace.package]
version = "0.5.2"
version = "0.5.3"
edition = "2024"
license = "MIT"
repository = "https://github.com/ColeMei/openwith"
Expand Down
2 changes: 1 addition & 1 deletion crates/openwith-gui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "openwith-gui",
"private": true,
"version": "0.5.2",
"version": "0.5.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
24 changes: 24 additions & 0 deletions crates/openwith-gui/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

use serde::Serialize;
use tauri::{AppHandle, Manager, State};
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};

use openwith_core::history::{self, HistoryEvent};
use openwith_core::types::AppInfo;
Expand All @@ -19,6 +21,28 @@ fn record_history(event: HistoryEvent) {
#[derive(Default)]
pub struct AppsCache(Mutex<Option<Arc<Vec<AppInfo>>>>);

/// While pinned the popover survives losing focus, so a file can be dragged
/// in from Finder (grabbing the file blurs the popover, which normally hides
/// it). Reset every time the popover is toggled open.
#[derive(Default)]
pub struct PopoverPinned(pub AtomicBool);

#[tauri::command]
pub fn set_popover_pinned(pinned: bool, state: State<'_, PopoverPinned>) {
state.0.store(pinned, Ordering::Relaxed);
}

/// Swap the global shortcut that toggles the popover. Only one toggle
/// shortcut exists at a time, so replacing means unregistering everything.
#[tauri::command]
pub fn set_toggle_shortcut(app: AppHandle, accelerator: String) -> Result<(), String> {
let shortcut: Shortcut = accelerator.parse().map_err(|e| format!("{e}"))?;
let shortcuts = app.global_shortcut();
shortcuts.unregister_all().map_err(|e| e.to_string())?;
shortcuts.register(shortcut).map_err(|e| e.to_string())?;
Ok(())
}

fn cached_apps(cache: &State<'_, AppsCache>) -> Result<Arc<Vec<AppInfo>>, String> {
let mut slot = cache.0.lock().expect("apps cache poisoned");
if let Some(apps) = slot.as_ref() {
Expand Down
28 changes: 22 additions & 6 deletions crates/openwith-gui/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
mod commands;
mod tray;

use std::sync::atomic::Ordering;

use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_global_shortcut::{Code, Modifiers, Shortcut, ShortcutState};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let toggle_shortcut = Shortcut::new(Some(Modifiers::ALT | Modifiers::SUPER), Code::KeyO);
// Default toggle shortcut; a saved custom one replaces it at bootstrap
// via the set_toggle_shortcut command. Only one shortcut is ever
// registered, so the handler doesn't need to know which combo it is.
let default_shortcut = Shortcut::new(Some(Modifiers::ALT | Modifiers::SUPER), Code::KeyO);

tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
Expand All @@ -18,20 +24,28 @@ pub fn run() {
))
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_shortcuts([toggle_shortcut])
.with_shortcuts([default_shortcut])
.expect("valid shortcut")
.with_handler(move |app, shortcut, event| {
if shortcut == &toggle_shortcut && event.state == ShortcutState::Pressed {
.with_handler(move |app, _shortcut, event| {
if event.state == ShortcutState::Pressed {
tray::toggle_popover(app);
}
})
.build(),
)
.manage(commands::AppsCache::default())
.manage(commands::PopoverPinned::default())
.on_window_event(|window, event| match (window.label(), event) {
// The popover behaves like a menu: clicking anywhere else closes it.
// The popover behaves like a menu: clicking anywhere else closes
// it — unless pinned, which keeps it up for drag-and-drop.
("menubar", tauri::WindowEvent::Focused(false)) => {
let _ = window.hide();
let pinned = window
.state::<commands::PopoverPinned>()
.0
.load(Ordering::Relaxed);
if !pinned {
let _ = window.hide();
}
}
// Standard macOS behavior: the close button hides the window, the
// app keeps running (⌘Q quits). Destroying it would make the app
Expand Down Expand Up @@ -59,6 +73,8 @@ pub fn run() {
commands::quit_app,
commands::set_tray_enabled,
commands::set_dock_visible,
commands::set_popover_pinned,
commands::set_toggle_shortcut,
])
.build(tauri::generate_context!())
.expect("error while building tauri application")
Expand Down
13 changes: 12 additions & 1 deletion crates/openwith-gui/src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use std::sync::atomic::Ordering;

use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Manager};
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_positioner::{Position, WindowExt};

use crate::commands::PopoverPinned;

const TRAY_ID: &str = "openwith-tray";

/// Add or remove the menu-bar icon. The app's tray registry is the source of
Expand Down Expand Up @@ -45,6 +49,10 @@ pub fn toggle_popover(app: &AppHandle) {
let Some(window) = app.get_webview_window("menubar") else {
return;
};
// Every toggle starts unpinned; pinning is a per-showing choice.
app.state::<PopoverPinned>()
.0
.store(false, Ordering::Relaxed);
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
return;
Expand All @@ -56,4 +64,7 @@ pub fn toggle_popover(app: &AppHandle) {
}
let _ = window.show();
let _ = window.set_focus();
// Focus events are unreliable for the transparent popover panel, so tell
// the webview explicitly that it just opened (refresh + reset pin UI).
let _ = window.emit("popover-shown", ());
}
4 changes: 4 additions & 0 deletions crates/openwith-gui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,8 @@ export const api = {
invoke<void>("set_tray_enabled", { enabled }),
setDockVisible: (visible: boolean) =>
invoke<void>("set_dock_visible", { visible }),
setPopoverPinned: (pinned: boolean) =>
invoke<void>("set_popover_pinned", { pinned }),
setToggleShortcut: (accelerator: string) =>
invoke<void>("set_toggle_shortcut", { accelerator }),
};
Loading
Loading