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
4 changes: 2 additions & 2 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"global-shortcut:allow-unregister",
"global-shortcut:allow-is-registered",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text"
"clipboard-manager:allow-read-text",
"core:window:allow-set-always-on-top"
]
}
9 changes: 9 additions & 0 deletions src-tauri/src/data/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,27 @@ pub struct GeneralSettings {
/// None means no hotkey is registered
#[serde(default = "default_hotkey")]
pub hotkey: Option<String>,
/// Whether the editor window floats above other windows
#[serde(default = "default_editor_always_on_top")]
pub editor_always_on_top: bool,
}

/// Default hotkey: Cmd/Ctrl+Shift+Space
fn default_hotkey() -> Option<String> {
Some("CommandOrControl+Shift+Space".to_string())
}

/// Default editor always on top: true
fn default_editor_always_on_top() -> bool {
true
}

impl Default for GeneralSettings {
fn default() -> Self {
Self {
auto_launch: false,
hotkey: default_hotkey(),
editor_always_on_top: default_editor_always_on_top(),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub fn run() {
os::paste::paste_from_editor,
os::paste::copy_as_markdown_file,
os::window::open_editor_window,
os::window::close_editor_window,
// Hotkey commands
os::hotkey::get_current_hotkey,
os::hotkey::set_hotkey,
Expand Down
13 changes: 13 additions & 0 deletions src-tauri/src/os/hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ use crate::data::settings::AppSettings;
use crate::os::focus::get_key_window_screen_bounds;
use crate::os::previous_app;

/// Close the editor window if it exists and always-on-top mode is enabled
fn close_editor_if_always_on_top(app: &AppHandle) {
let settings = AppSettings::load();
if settings.general.editor_always_on_top {
if let Some(editor) = app.get_webview_window("editor") {
let _ = editor.close();
}
}
}

const WINDOW_WIDTH: f64 = 650.0;

/// State to track the currently registered shortcut
Expand Down Expand Up @@ -173,6 +183,9 @@ pub fn register_hotkey(app: &AppHandle, hotkey_str: &str) -> Result<(), String>
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
} else {
// Close editor window if in always-on-top mode (they shouldn't coexist)
close_editor_if_always_on_top(&app_handle);

// Capture previous app before showing (for paste-back feature)
if let Err(e) = previous_app::capture_previous_app() {
eprintln!("[hotkey] Failed to capture previous app: {}", e);
Expand Down
31 changes: 30 additions & 1 deletion src-tauri/src/os/window.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use serde::Deserialize;
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};

use crate::data::settings::AppSettings;

/// Screen bounds passed from frontend for window positioning
#[derive(Debug, Clone, Deserialize)]
pub struct ScreenBounds {
Expand All @@ -13,18 +15,27 @@ pub struct ScreenBounds {
/// Open the editor window, optionally loading a specific prompt
/// If screen_bounds is provided, the window will be centered on that screen
/// view parameter can be "prompts" (default) or "settings"
/// always_on_top parameter overrides the setting (used when opening from launcher)
#[tauri::command]
pub async fn open_editor_window(
app: AppHandle,
prompt_id: Option<String>,
screen_bounds: Option<ScreenBounds>,
view: Option<String>,
always_on_top: Option<bool>,
) -> Result<(), String> {
let label = "editor";
let view_mode = view.as_deref().unwrap_or("prompts");

// Get always_on_top from parameter or settings
let settings = AppSettings::load();
let is_always_on_top = always_on_top.unwrap_or(settings.general.editor_always_on_top);

// If window already exists, show it and optionally emit event to load prompt or switch view
if let Some(window) = app.get_webview_window(label) {
// Update always-on-top state in case setting changed
window.set_always_on_top(is_always_on_top).map_err(|e| e.to_string())?;

window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;

Expand Down Expand Up @@ -58,7 +69,16 @@ pub async fn open_editor_window(
let mut builder = WebviewWindowBuilder::new(&app, label, WebviewUrl::App(url.into()))
.title("PromptLight Editor")
.inner_size(window_width, window_height)
.min_inner_size(800.0, 600.0);
.min_inner_size(800.0, 600.0)
.always_on_top(is_always_on_top);

// In always-on-top mode, hide minimize/maximize buttons (macOS)
#[cfg(target_os = "macos")]
if is_always_on_top {
builder = builder
.minimizable(false)
.maximizable(false);
}

// Position on provided screen or center as fallback
if let Some(bounds) = screen_bounds {
Expand All @@ -73,3 +93,12 @@ pub async fn open_editor_window(

Ok(())
}

/// Close the editor window if it exists
#[tauri::command]
pub fn close_editor_window(app: AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("editor") {
window.close().map_err(|e| e.to_string())?;
}
Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,19 @@ export function MarkdownEditor({
// Clear any existing content (handles React StrictMode double-mount)
containerRef.current.innerHTML = '';

// Create keyboard shortcuts for bold (Cmd+B) and italic (Cmd+I)
// Create keyboard shortcuts for formatting and selection
const formattingKeymap = keymap.of([
{
key: 'Mod-a',
run: () => {
// Use ink-mde's native API to select all text
if (editorRef.current) {
const doc = editorRef.current.getDoc();
editorRef.current.select({ selections: [{ start: 0, end: doc.length }] });
}
return true;
},
},
{
key: 'Mod-b',
run: () => {
Expand Down
20 changes: 18 additions & 2 deletions src/components/editor/Sidebar/Sidebar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
min-width: 40px;
background: var(--bg-secondary);
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
align-items: center;
gap: var(--space-xs);
padding-top: var(--space-md);
}

Expand All @@ -33,6 +34,21 @@
color: var(--text-primary);
}

.collapsedNewButton {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: var(--space-sm);
border-radius: var(--radius-sm);
transition: background var(--transition-fast), color var(--transition-fast);
}

.collapsedNewButton:hover {
background: var(--selection-bg);
color: var(--accent-primary);
}

.header {
display: flex;
align-items: center;
Expand Down
7 changes: 7 additions & 0 deletions src/components/editor/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ export function Sidebar() {
>
<Menu size={16} />
</button>
<button
className={styles.collapsedNewButton}
onClick={createNew}
title="New prompt (Cmd+N)"
>
<Plus size={16} />
</button>
</div>
);
}
Expand Down
30 changes: 29 additions & 1 deletion src/components/editor/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Settings, Cloud, CloudOff, Power, LogOut, User, Keyboard, Palette, Sun, Moon, Monitor } from 'lucide-react';
import { Settings, Cloud, CloudOff, Power, LogOut, User, Keyboard, Palette, Sun, Moon, Monitor, Layers } from 'lucide-react';
import { getVersion } from '@tauri-apps/api/app';
import { useSettingsStore, type AppearanceSettings } from '../../../stores/settingsStore';
import { useAuthStore } from '../../../stores/authStore';
Expand All @@ -22,6 +22,7 @@ export function SettingsView() {
updateSyncSettings,
setAutoLaunch,
setHotkey,
setEditorAlwaysOnTop,
setTheme,
setAccentColor,
} = useSettingsStore();
Expand Down Expand Up @@ -88,6 +89,8 @@ export function SettingsView() {
onAutoLaunchChange={setAutoLaunch}
hotkey={settings.general.hotkey}
onHotkeyChange={setHotkey}
editorAlwaysOnTop={settings.general.editorAlwaysOnTop}
onEditorAlwaysOnTopChange={setEditorAlwaysOnTop}
isSaving={isSaving}
/>
)}
Expand Down Expand Up @@ -123,6 +126,8 @@ interface GeneralSectionProps {
onAutoLaunchChange: (value: boolean) => void;
hotkey: string | null;
onHotkeyChange: (value: string | null) => void;
editorAlwaysOnTop: boolean;
onEditorAlwaysOnTopChange: (value: boolean) => void;
isSaving: boolean;
}

Expand All @@ -131,6 +136,8 @@ function GeneralSection({
onAutoLaunchChange,
hotkey,
onHotkeyChange,
editorAlwaysOnTop,
onEditorAlwaysOnTopChange,
isSaving,
}: GeneralSectionProps) {
return (
Expand All @@ -154,6 +161,27 @@ function GeneralSection({
<HotkeyInput value={hotkey} onChange={onHotkeyChange} disabled={isSaving} />
</div>

<div className={styles.settingRow}>
<div className={styles.settingInfo}>
<div className={styles.settingLabel}>
<Layers size={16} />
Floating editor
</div>
<div className={styles.settingHint}>
Keep editor above other windows. Opening the launcher will close the editor.
</div>
</div>
<label className={styles.toggle}>
<input
type="checkbox"
checked={editorAlwaysOnTop}
onChange={(e) => onEditorAlwaysOnTopChange(e.target.checked)}
data-testid="editor-always-on-top-toggle"
/>
<span className={styles.toggleSlider} />
</label>
</div>

<div className={styles.settingRow}>
<div className={styles.settingInfo}>
<div className={styles.settingLabel}>
Expand Down
17 changes: 1 addition & 16 deletions src/hooks/useEditorKeyboard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useEffect } from 'react';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { useEditorStore } from '../stores/editorStore';
import { HOTKEYS } from '../config/constants';

Expand All @@ -11,21 +10,7 @@ export function useEditorKeyboard() {

useEffect(() => {
const handleKeyDown = async (e: KeyboardEvent) => {
// Escape key - save (if dirty) then close window
if (e.key === 'Escape') {
e.preventDefault();

// Auto-save is always on, but save explicitly if dirty
if (isDirty) {
await save();
}

// Close the editor window
await getCurrentWindow().close();
return;
}

// Only handle Cmd/Ctrl combinations below
// Only handle Cmd/Ctrl combinations
if (!(e.metaKey || e.ctrlKey)) {
return;
}
Expand Down
1 change: 1 addition & 0 deletions src/services/backend/authTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface AuthSession {
export interface GeneralSettings {
autoLaunch: boolean;
hotkey: string | null;
editorAlwaysOnTop: boolean;
}

/** Cloud sync settings */
Expand Down
1 change: 1 addition & 0 deletions src/services/backend/mockAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const defaultSettings: AppSettings = {
general: {
autoLaunch: false,
hotkey: 'CommandOrControl+Shift+Space',
editorAlwaysOnTop: true,
},
sync: {
enabled: false,
Expand Down
16 changes: 15 additions & 1 deletion src/stores/settingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface GeneralSettings {
autoLaunch: boolean;
/** Global hotkey to summon the launcher (e.g., "CommandOrControl+Shift+Space") */
hotkey: string | null;
/** Whether the editor window should float above other windows (default: true) */
editorAlwaysOnTop: boolean;
}

/** Cloud sync settings */
Expand Down Expand Up @@ -60,6 +62,8 @@ interface SettingsActions {
setAutoLaunch: (enabled: boolean) => Promise<void>;
/** Set global hotkey (null to disable) */
setHotkey: (hotkey: string | null) => Promise<void>;
/** Set editor always-on-top mode */
setEditorAlwaysOnTop: (enabled: boolean) => Promise<void>;
/** Set theme */
setTheme: (theme: ThemeOption) => Promise<void>;
/** Set accent color */
Expand All @@ -74,6 +78,7 @@ const defaultSettings: AppSettings = {
general: {
autoLaunch: false,
hotkey: 'CommandOrControl+Shift+Space',
editorAlwaysOnTop: true,
},
sync: {
enabled: false,
Expand Down Expand Up @@ -119,9 +124,13 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
backend.getSettings(),
backend.getAutoStartEnabled().catch(() => false),
]);
// Ensure appearance has defaults if missing (backwards compat)
// Ensure fields have defaults if missing (backwards compat)
const normalizedSettings: AppSettings = {
...settings,
general: {
...settings.general,
editorAlwaysOnTop: settings.general?.editorAlwaysOnTop ?? true,
},
appearance: {
theme: settings.appearance?.theme ?? DEFAULT_THEME,
accentColor: settings.appearance?.accentColor ?? DEFAULT_ACCENT_COLOR,
Expand Down Expand Up @@ -250,6 +259,11 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
}
},

setEditorAlwaysOnTop: async (enabled) => {
const { updateGeneralSettings } = get();
await updateGeneralSettings({ editorAlwaysOnTop: enabled });
},

clearError: () => {
set({ error: null });
},
Expand Down
Loading