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
Binary file added public/welcome-artwork.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src-tauri/src/data/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pub struct GeneralSettings {
/// Whether the editor window floats above other windows
#[serde(default = "default_editor_always_on_top")]
pub editor_always_on_top: bool,
/// Whether the welcome screen has been dismissed (don't show again)
#[serde(default)]
pub welcome_screen_dismissed: bool,
}

/// Default hotkey: Cmd/Ctrl+Shift+Space
Expand All @@ -33,6 +36,7 @@ impl Default for GeneralSettings {
auto_launch: false,
hotkey: default_hotkey(),
editor_always_on_top: default_editor_always_on_top(),
welcome_screen_dismissed: false,
}
}
}
Expand Down
34 changes: 34 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ pub fn run() {
}
}

// Check if welcome screen should show
let show_welcome = crate::os::welcome::should_show_welcome();

if show_welcome {
// Hide the launcher (it starts hidden in tauri.conf.json but may be visible)
if let Some(launcher) = app.get_webview_window("launcher") {
let _ = launcher.hide();
}

// Open welcome window
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
if let Err(e) = crate::os::welcome::open_welcome_window(app_handle).await {
eprintln!("Failed to open welcome window: {}", e);
}
});
} else {
// Show launcher immediately if welcome is dismissed
if let Some(launcher) = app.get_webview_window("launcher") {
let _ = launcher.show();
}
}

// Register global shortcut from settings (defaults to Cmd+Shift+Space)
let app_handle = app.handle();
if let Err(e) = init_hotkey_from_settings(app_handle) {
Expand Down Expand Up @@ -126,6 +149,9 @@ pub fn run() {
os::paste::copy_as_markdown_file,
os::window::open_editor_window,
os::window::close_editor_window,
// Welcome window commands
os::welcome::open_welcome_window,
os::welcome::close_welcome_window,
// Hotkey commands
os::hotkey::get_current_hotkey,
os::hotkey::set_hotkey,
Expand All @@ -142,6 +168,14 @@ pub fn run() {
// Handle dock icon click on macOS
#[cfg(target_os = "macos")]
if let RunEvent::Reopen { .. } = _event {
// If welcome window is visible, focus it
if crate::os::welcome::is_welcome_visible(_app_handle) {
if let Some(welcome) = _app_handle.get_webview_window("welcome") {
let _ = welcome.set_focus();
}
return;
}

// Check if editor window exists and is visible
if let Some(editor) = _app_handle.get_webview_window("editor") {
if editor.is_visible().unwrap_or(false) {
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/os/hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ pub fn register_hotkey(app: &AppHandle, hotkey_str: &str) -> Result<(), String>
return;
}

// Dismiss welcome window if visible
if crate::os::welcome::is_welcome_visible(&app_handle) {
if let Some(welcome) = app_handle.get_webview_window("welcome") {
let _ = welcome.close();
}
// Note: Don't save dismissal preference here - let user decide via checkbox
}

if let Some(window) = app_handle.get_webview_window("launcher") {
// Toggle window visibility
if window.is_visible().unwrap_or(false) {
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/os/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pub mod hotkey;
pub mod paste;
pub mod platform;
pub mod previous_app;
pub mod welcome;
pub mod window;
114 changes: 114 additions & 0 deletions src-tauri/src/os/welcome.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};

use crate::data::settings::AppSettings;
use crate::os::focus::get_key_window_screen_bounds;

const WELCOME_WIDTH: f64 = 900.0;
const WELCOME_HEIGHT: f64 = 604.0;

/// Check if welcome screen should be shown based on settings
pub fn should_show_welcome() -> bool {
let settings = AppSettings::load();
!settings.general.welcome_screen_dismissed
}

/// Check if welcome window is currently visible
pub fn is_welcome_visible(app: &AppHandle) -> bool {
app.get_webview_window("welcome")
.map(|w| w.is_visible().unwrap_or(false))
.unwrap_or(false)
}

/// Open the welcome screen window
#[tauri::command]
pub async fn open_welcome_window(app: AppHandle) -> Result<(), String> {
let label = "welcome";

// If window already exists, just show and focus it
if let Some(window) = app.get_webview_window(label) {
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
return Ok(());
}

// Create the welcome window
let mut builder = WebviewWindowBuilder::new(
&app,
label,
WebviewUrl::App("index.html?window=welcome".into()),
)
.title("Welcome to PromptLight")
.inner_size(WELCOME_WIDTH, WELCOME_HEIGHT)
.resizable(false)
.decorations(false)
.always_on_top(true);

// Position on the screen with the active/key window
if let Some(bounds) = get_key_window_screen_bounds() {
let x = bounds.x + (bounds.width - WELCOME_WIDTH) / 2.0;
let y = bounds.y + (bounds.height - WELCOME_HEIGHT) / 2.0;
builder = builder.position(x, y);
} else {
builder = builder.center();
}

// Disable minimize/maximize on macOS
#[cfg(target_os = "macos")]
{
builder = builder.minimizable(false).maximizable(false);
}

let window = builder.build().map_err(|e| e.to_string())?;

// Apply transparent background (required for rounded corners on macOS)
#[cfg(target_os = "macos")]
{
use cocoa::appkit::{NSColor, NSWindow};
use cocoa::base::{id, nil};
use cocoa::foundation::NSString;

// First, disable the webview's background drawing
let _ = window.with_webview(|webview| {
unsafe {
let wv: id = webview.inner().cast();
let no: id = msg_send![class!(NSNumber), numberWithBool: false];
let key = NSString::alloc(nil).init_str("drawsBackground");
let _: () = msg_send![wv, setValue: no forKey: key];
}
});

// Then set the window background to clear
let ns_window = window.ns_window().unwrap() as id;
unsafe {
let clear_color: id = NSColor::clearColor(nil);
NSWindow::setBackgroundColor_(ns_window, clear_color);
let _: () = msg_send![ns_window, setOpaque: false];
}
}

Ok(())
}

/// Close the welcome window and optionally save the "don't show again" preference
#[tauri::command]
pub fn close_welcome_window(app: AppHandle, dont_show_again: bool) -> Result<(), String> {
// Save preference if "Don't show again" was checked
if dont_show_again {
let mut settings = AppSettings::load();
settings.general.welcome_screen_dismissed = true;
settings.save()?;
}

// Close the window
if let Some(window) = app.get_webview_window("welcome") {
window.close().map_err(|e| e.to_string())?;
}

// Show the launcher window
if let Some(launcher) = app.get_webview_window("launcher") {
launcher.show().map_err(|e| e.to_string())?;
launcher.set_focus().map_err(|e| e.to_string())?;
}

Ok(())
}
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"transparent": true,
"backgroundColor": [0, 0, 0, 0],
"alwaysOnTop": true,
"visible": true,
"visible": false,
"skipTaskbar": true,
"focus": true
}
Expand Down
9 changes: 8 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
import { LauncherWindow } from './components/launcher';
import { EditorWindow } from './components/editor';
import { WelcomeWindow } from './components/welcome';
import { MarkdownEditorTest } from './components/editor/PromptEditor/MarkdownEditor/MarkdownEditorTest';
import { useLauncher, useUIScale } from './hooks';
import { useSettingsStore } from './stores/settingsStore';

type WindowType = 'launcher' | 'editor' | 'test' | null;
type WindowType = 'launcher' | 'editor' | 'welcome' | 'test' | null;

function App() {
const [windowType, setWindowType] = useState<WindowType>(null);
Expand All @@ -25,6 +26,8 @@ function App() {
const windowParam = params.get('window');
if (windowParam === 'editor') {
setWindowType('editor');
} else if (windowParam === 'welcome') {
setWindowType('welcome');
} else if (windowParam === 'test') {
setWindowType('test');
} else {
Expand All @@ -42,6 +45,10 @@ function App() {
return <EditorWindow />;
}

if (windowType === 'welcome') {
return <WelcomeWindow />;
}

if (windowType === 'test') {
return <MarkdownEditorTest />;
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/launcher/ResultItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export function ResultItem({ result, isSelected, index }: ResultItemProps) {
executeSelected();
};

const handleMouseEnter = () => {
setSelectedIndex(index);
};

const handleContextMenu = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
Expand All @@ -44,6 +48,7 @@ export function ResultItem({ result, isSelected, index }: ResultItemProps) {
ref={itemRef}
className={`${styles.item} ${isSelected ? styles.selected : ''}`}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onContextMenu={handleContextMenu}
role="option"
aria-selected={isSelected}
Expand Down
75 changes: 75 additions & 0 deletions src/components/welcome/WelcomeWindow.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
.container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
border-radius: var(--radius-lg);
background: #1a1a1a;
}

.artwork {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}

.controls {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
}

.letsGoButton {
padding: var(--space-3) var(--space-8);
background: #6b7c3f;
color: #ffffff;
border: none;
border-radius: var(--radius-md);
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: background var(--transition-fast), transform var(--transition-fast);
min-width: 160px;
}

.letsGoButton:hover:not(:disabled) {
background: #7a8c4a;
transform: scale(1.02);
}

.letsGoButton:active:not(:disabled) {
transform: scale(0.98);
}

.letsGoButton:disabled {
opacity: 0.7;
cursor: not-allowed;
}

.letsGoButton:focus-visible {
outline: 2px solid #6b7c3f;
outline-offset: 2px;
}

.checkboxLabel {
display: flex;
align-items: center;
gap: var(--space-2);
color: rgba(255, 255, 255, 0.85);
font-size: var(--font-size-sm);
cursor: pointer;
user-select: none;
}

.checkbox {
width: 16px;
height: 16px;
accent-color: #6b7c3f;
cursor: pointer;
}
Loading
Loading