11 Commits

10 changed files with 64 additions and 350 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,38 +0,0 @@
---
description:
globs:
alwaysApply: true
---
You are an expert AI programming assistant focused on producing clean, readable TypeScript and Rust code for modern cross-platform desktop apps.
Use these rules for any code under /frontend folder.
You always use the latest versions of Tauri, Rust, SolidJS, and you're fluent in their latest features, best practices, and patterns.
You give accurate, thoughtful answers and think like a real dev—step-by-step.
Follow the users specs exactly. If a specs folder exists, check it before coding.
Begin with a detailed pseudo-code plan and confirm it with the user before writing actual code.
Write correct, complete, idiomatic, secure, performant, and bug-free code.
Prioritize readability unless performance is explicitly required.
Fully implement all requested features—no TODOs, stubs, or placeholders.
Use TypeScript's type system thoroughly for clarity and safety.
Style with TailwindCSS using utility-first principles.
Use Kobalte components effectively, building with Solids reactive model in mind.
Offload performance-heavy logic to Rust and ensure smooth integration with Tauri.
Guarantee tight coordination between SolidJS, Tauri, and Rust for a polished desktop UX.
When needed, provide bash scripts to generate config files or folder structures.
Be concise—cut the fluff.
If there's no solid answer, say so. If you're unsure, don't guess—own it.

View File

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="NONE" />
</component>
<component name="ChangeListManager">
<list default="true" id="4ea94c05-c21c-40f9-ad16-43233a3011ee" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 5
}</component>
<component name="ProjectId" id="2w23zazSC8gW9XDwUxbl8Fam8DV" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.readMode.enableVisualFormatting": "true",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
"com.google.services.firebase.aqiPopupShown": "true",
"git-widget-placeholder": "feat/android-version",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "/home/johnc/Code/haystack-app/frontend",
"settings.editor.selected.configurable": "AndroidSdkUpdater"
}
}]]></component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="4ea94c05-c21c-40f9-ad16-43233a3011ee" name="Changes" comment="" />
<created>1745226104717</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1745226104717</updated>
</task>
<servers />
</component>
</project>

Binary file not shown.

View File

@ -7,7 +7,6 @@ name = "Haystack"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"chrono",
"cocoa", "cocoa",
"notify", "notify",
"serde", "serde",
@ -539,10 +538,8 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen",
"windows-link", "windows-link",
] ]

View File

@ -26,9 +26,6 @@ base64 = "0.21.7"
tokio = { version = "1.36.0", features = ["full"] } tokio = { version = "1.36.0", features = ["full"] }
tauri-plugin-store = "2.0.0-beta.12" tauri-plugin-store = "2.0.0-beta.12"
tauri-plugin-http = "2.0.0-beta.12" tauri-plugin-http = "2.0.0-beta.12"
chrono = "0.4"
tauri-plugin-log = "2"
tauri-plugin-sharetarget = "0.1.6"
tauri-plugin-fs = "2" tauri-plugin-fs = "2"
tauri-plugin-dialog = "2.2.1" tauri-plugin-dialog = "2.2.1"
tauri-plugin-opener = "2.2.6" tauri-plugin-opener = "2.2.6"

View File

@ -1,8 +1,6 @@
mod commands; mod commands;
pub mod screenshot;
pub mod shortcut;
mod state; mod state;
pub mod utils; mod utils;
mod window; mod window;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
@ -19,18 +17,14 @@ pub fn desktop() {
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_sharetarget::init()) .plugin(tauri_plugin_dialog::init())
// .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init())
// .plugin(tauri_plugin_opener::init())
.manage(watcher_state) .manage(watcher_state)
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::handle_selected_folder, commands::handle_selected_folder,
shortcut::change_shortcut, shortcut::change_shortcut,
shortcut::unregister_shortcut, shortcut::unregister_shortcut,
shortcut::get_current_shortcut, shortcut::get_current_shortcut,
shortcut::change_screenshot_shortcut,
shortcut::unregister_screenshot_shortcut,
shortcut::get_current_screenshot_shortcut,
]) ])
.setup(|app| { .setup(|app| {
setup_window(app)?; setup_window(app)?;

View File

@ -1,46 +0,0 @@
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use std::fs;
use std::process::Command;
use tauri::{AppHandle, Emitter, Runtime};
/// Takes a screenshot of a selected area and returns the image data as base64
pub fn take_area_screenshot<R: Runtime>(app: &AppHandle<R>) -> Result<String, String> {
// Create a temporary file path
let temp_dir = std::env::temp_dir();
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
let temp_file = temp_dir.join(format!("haystack_screenshot_{}.png", timestamp));
// Use screencapture command with -i flag for interactive selection
let output = Command::new("screencapture")
.arg("-i") // interactive selection
.arg("-x") // don't play sound
.arg("-o") // don't show cursor
.arg("-r") // don't add shadow
.arg(temp_file.to_str().unwrap())
.output()
.map_err(|e| format!("Failed to execute screencapture: {}", e))?;
if !output.status.success() {
return Err(format!(
"screencapture failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
// Read the captured image
let contents =
fs::read(&temp_file).map_err(|e| format!("Failed to read screenshot file: {}", e))?;
// Convert to base64
let base64_string = BASE64.encode(&contents);
// Clean up the temporary file
if let Err(e) = fs::remove_file(&temp_file) {
println!("Warning: Failed to remove temporary screenshot file: {}", e);
}
app.emit("png-processed", base64_string.clone())
.map_err(|e| format!("Failed to emit event: {}", e))?;
Ok(base64_string)
}

View File

@ -1,4 +1,3 @@
use crate::screenshot::take_area_screenshot;
use tauri::App; use tauri::App;
use tauri::AppHandle; use tauri::AppHandle;
use tauri::Emitter; use tauri::Emitter;
@ -10,36 +9,28 @@ use tauri_plugin_global_shortcut::ShortcutState;
use tauri_plugin_store::JsonValue; use tauri_plugin_store::JsonValue;
use tauri_plugin_store::StoreExt; use tauri_plugin_store::StoreExt;
/// Constants for Tauri store configuration /// Name of the Tauri storage
const HAYSTACK_TAURI_STORE: &str = "haystack_tauri_store"; // Name of the persistent store const HAYSTACK_TAURI_STORE: &str = "haystack_tauri_store";
const HAYSTACK_GLOBAL_SHORTCUT: &str = "haystack_global_shortcut"; // Key for storing the toggle window shortcut
const HAYSTACK_SCREENSHOT_SHORTCUT: &str = "haystack_screenshot_shortcut"; // Key for storing the screenshot shortcut
/// Platform-specific default shortcuts /// Key for storing global shortcuts
const HAYSTACK_GLOBAL_SHORTCUT: &str = "haystack_global_shortcut";
/// Default shortcut for macOS
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const DEFAULT_SHORTCUT: &str = "command+shift+k"; // macOS uses Command key const DEFAULT_SHORTCUT: &str = "command+shift+k";
#[cfg(target_os = "macos")]
const DEFAULT_SCREENSHOT_SHORTCUT: &str = "command+shift+p"; // macOS screenshot shortcut
/// Default shortcut for Windows and Linux
#[cfg(any(target_os = "windows", target_os = "linux"))] #[cfg(any(target_os = "windows", target_os = "linux"))]
const DEFAULT_SHORTCUT: &str = "ctrl+shift+k"; // Windows/Linux use Ctrl key const DEFAULT_SHORTCUT: &str = "ctrl+shift+k";
#[cfg(any(target_os = "windows", target_os = "linux"))]
const DEFAULT_SCREENSHOT_SHORTCUT: &str = "ctrl+shift+p"; // Windows/Linux screenshot shortcut
/// Initializes the global shortcut during application startup. /// Set shortcut during application startup
/// This function:
/// 1. Checks if a shortcut is already stored
/// 2. Uses the stored shortcut if it exists
/// 3. Falls back to the platform-specific default if no shortcut is stored
/// 4. Registers the shortcut with the system
pub fn enable_shortcut(app: &App) { pub fn enable_shortcut(app: &App) {
// Get or create the persistent store
let store = app let store = app
.store(HAYSTACK_TAURI_STORE) .store(HAYSTACK_TAURI_STORE)
.expect("Creating the store should not fail"); .expect("Creating the store should not fail");
// Initialize toggle window shortcut // Use stored shortcut if it exists
let toggle_shortcut = if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) { if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) {
let stored_shortcut_str = match stored_shortcut { let stored_shortcut_str = match stored_shortcut {
JsonValue::String(str) => str, JsonValue::String(str) => str,
unexpected_type => panic!( unexpected_type => panic!(
@ -47,72 +38,45 @@ pub fn enable_shortcut(app: &App) {
unexpected_type unexpected_type
), ),
}; };
stored_shortcut_str let stored_shortcut = stored_shortcut_str
.parse::<Shortcut>() .parse::<Shortcut>()
.expect("Stored shortcut string should be valid") .expect("Stored shortcut string should be valid");
_register_shortcut_upon_start(app, stored_shortcut); // Register stored shortcut
} else { } else {
// Use default shortcut if none is stored
store.set( store.set(
HAYSTACK_GLOBAL_SHORTCUT, HAYSTACK_GLOBAL_SHORTCUT,
JsonValue::String(DEFAULT_SHORTCUT.to_string()), JsonValue::String(DEFAULT_SHORTCUT.to_string()),
); );
DEFAULT_SHORTCUT let default_shortcut = DEFAULT_SHORTCUT
.parse::<Shortcut>() .parse::<Shortcut>()
.expect("Default shortcut should be valid") .expect("Default shortcut should be valid");
}; _register_shortcut_upon_start(app, default_shortcut); // Register default shortcut
}
// Initialize screenshot shortcut
let screenshot_shortcut = if let Some(stored_shortcut) = store.get(HAYSTACK_SCREENSHOT_SHORTCUT)
{
let stored_shortcut_str = match stored_shortcut {
JsonValue::String(str) => str,
unexpected_type => panic!(
"Haystack shortcuts should be stored as strings, found type: {} ",
unexpected_type
),
};
stored_shortcut_str
.parse::<Shortcut>()
.expect("Stored shortcut string should be valid")
} else {
store.set(
HAYSTACK_SCREENSHOT_SHORTCUT,
JsonValue::String(DEFAULT_SCREENSHOT_SHORTCUT.to_string()),
);
DEFAULT_SCREENSHOT_SHORTCUT
.parse::<Shortcut>()
.expect("Default screenshot shortcut should be valid")
};
// Register both shortcuts
register_shortcut_upon_start(app, toggle_shortcut, screenshot_shortcut);
} }
/// Returns the currently configured shortcut as a string. /// Get the current stored shortcut as a string
/// This is exposed as a Tauri command for the frontend to query.
#[tauri::command] #[tauri::command]
pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> { pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
Ok(get_shortcut_from_store(&app)) let shortcut = _get_shortcut(&app);
Ok(shortcut)
} }
/// Unregisters the current global shortcut from the system. /// Unregister the current shortcut in Tauri
/// This is exposed as a Tauri command for the frontend to trigger.
#[tauri::command] #[tauri::command]
pub fn unregister_shortcut<R: Runtime>(app: AppHandle<R>) { pub fn unregister_shortcut<R: Runtime>(app: AppHandle<R>) {
let shortcut_str = get_shortcut_from_store(&app); let shortcut_str = _get_shortcut(&app);
let shortcut = shortcut_str let shortcut = shortcut_str
.parse::<Shortcut>() .parse::<Shortcut>()
.expect("Stored shortcut string should be valid"); .expect("Stored shortcut string should be valid");
// Unregister the shortcut
app.global_shortcut() app.global_shortcut()
.unregister(shortcut) .unregister(shortcut)
.expect("Failed to unregister shortcut") .expect("Failed to unregister shortcut")
} }
/// Changes the global shortcut to a new key combination. /// Change the global shortcut
/// This function:
/// 1. Validates the new shortcut
/// 2. Stores it in the persistent store
/// 3. Registers it with the system
#[tauri::command] #[tauri::command]
pub fn change_shortcut<R: Runtime>( pub fn change_shortcut<R: Runtime>(
app: AppHandle<R>, app: AppHandle<R>,
@ -132,40 +96,28 @@ pub fn change_shortcut<R: Runtime>(
store.set(HAYSTACK_GLOBAL_SHORTCUT, JsonValue::String(key)); store.set(HAYSTACK_GLOBAL_SHORTCUT, JsonValue::String(key));
// Register the new shortcut // Register the new shortcut
register_shortcut(&app, shortcut); _register_shortcut(&app, shortcut);
Ok(()) Ok(())
} }
/// Handles the window visibility toggle logic when the shortcut is pressed. /// Helper function to register a shortcut, primarily for updating shortcuts
/// This function: fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
/// 1. Hides the window if it's visible
/// 2. Shows and focuses the window if it's hidden
/// 3. Emits a 'focus-search' event when showing the window
fn handle_window_visibility<R: Runtime>(app: &AppHandle<R>, window: &tauri::WebviewWindow<R>) {
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
app.emit("focus-search", ()).unwrap();
}
}
/// Registers a new shortcut with the system.
/// This is used when changing shortcuts during runtime.
/// The function:
/// 1. Gets the main window
/// 2. Sets up the shortcut handler
/// 3. Registers the shortcut with the system
fn register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
let main_window = app.get_webview_window("main").unwrap(); let main_window = app.get_webview_window("main").unwrap();
// Register global shortcut and define its behavior
app.global_shortcut() app.global_shortcut()
.on_shortcut(shortcut, move |app, scut, event| { .on_shortcut(shortcut, move |app, scut, event| {
if scut == &shortcut { if scut == &shortcut {
if let ShortcutState::Pressed = event.state() { if let ShortcutState::Pressed = event.state() {
handle_window_visibility(app, &main_window); // Toggle window visibility
if main_window.is_visible().unwrap() {
main_window.hide().unwrap(); // Hide window
} else {
main_window.show().unwrap(); // Show window
main_window.set_focus().unwrap(); // Focus window
// Emit focus-search event
app.emit("focus-search", ()).unwrap();
}
} }
} }
}) })
@ -173,49 +125,39 @@ fn register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
.unwrap(); .unwrap();
} }
/// Registers a shortcut during application startup. /// Helper function to register shortcuts during application startup
/// This is similar to register_shortcut but handles the initial plugin setup. fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
/// The function:
/// 1. Gets the main window
/// 2. Sets up the global shortcut plugin
/// 3. Registers the shortcut with the system
fn register_shortcut_upon_start(
app: &App,
toggle_shortcut: Shortcut,
screenshot_shortcut: Shortcut,
) {
let window = app let window = app
.get_webview_window("main") .get_webview_window("main")
.expect("webview to be defined"); .expect("webview to be defined");
// Initialize global shortcut and set its handler
app.handle() app.handle()
.plugin( .plugin(
tauri_plugin_global_shortcut::Builder::new() tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |app, scut, event| { .with_handler(move |app, scut, event| {
if scut == &toggle_shortcut { if scut == &shortcut {
if let ShortcutState::Pressed = event.state() { if let ShortcutState::Pressed = event.state() {
handle_window_visibility(app, &window); // Toggle window visibility
if window.is_visible().unwrap() {
window.hide().unwrap(); // Hide window
} else {
window.show().unwrap(); // Show window
window.set_focus().unwrap(); // Focus window
// Emit focus-search event
app.emit("focus-search", ()).unwrap();
} }
} else if scut == &screenshot_shortcut {
if let ShortcutState::Pressed = event.state() {
// TODO: Implement screenshot functionality
println!("Screenshot shortcut pressed");
}
if let Err(e) = take_area_screenshot(app) {
println!("Failed to take screenshot: {}", e);
} }
} }
}) })
.build(), .build(),
) )
.unwrap(); .unwrap();
app.global_shortcut().register(toggle_shortcut).unwrap(); app.global_shortcut().register(shortcut).unwrap(); // Register global shortcut
app.global_shortcut().register(screenshot_shortcut).unwrap();
} }
/// Retrieves the currently stored shortcut from the persistent store. /// Retrieve the stored global shortcut as a string
/// This is a helper function used by other functions to access the stored shortcut. pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
fn get_shortcut_from_store<R: Runtime>(app: &AppHandle<R>) -> String {
let store = app let store = app
.get_store(HAYSTACK_TAURI_STORE) .get_store(HAYSTACK_TAURI_STORE)
.expect("Store should already be loaded or created"); .expect("Store should already be loaded or created");
@ -231,73 +173,3 @@ fn get_shortcut_from_store<R: Runtime>(app: &AppHandle<R>) -> String {
), ),
} }
} }
#[tauri::command]
pub fn get_current_screenshot_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
Ok(get_screenshot_shortcut_from_store(&app))
}
#[tauri::command]
pub fn unregister_screenshot_shortcut<R: Runtime>(app: AppHandle<R>) {
let shortcut_str = get_screenshot_shortcut_from_store(&app);
let shortcut = shortcut_str
.parse::<Shortcut>()
.expect("Stored shortcut string should be valid");
app.global_shortcut()
.unregister(shortcut)
.expect("Failed to unregister screenshot shortcut")
}
#[tauri::command]
pub fn change_screenshot_shortcut<R: Runtime>(
app: AppHandle<R>,
_window: tauri::Window<R>,
key: String,
) -> Result<(), String> {
let shortcut = match key.parse::<Shortcut>() {
Ok(shortcut) => shortcut,
Err(_) => return Err(format!("Invalid screenshot shortcut {}", key)),
};
let store = app
.get_store(HAYSTACK_TAURI_STORE)
.expect("Store should already be loaded or created");
store.set(HAYSTACK_SCREENSHOT_SHORTCUT, JsonValue::String(key));
register_screenshot_shortcut(&app, shortcut);
Ok(())
}
fn register_screenshot_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
app.global_shortcut()
.on_shortcut(shortcut, move |app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
if let Err(e) = take_area_screenshot(app) {
println!("Failed to take screenshot: {}", e);
}
}
}
})
.map_err(|err| format!("Failed to register new screenshot shortcut '{}'", err))
.unwrap();
}
fn get_screenshot_shortcut_from_store<R: Runtime>(app: &AppHandle<R>) -> String {
let store = app
.get_store(HAYSTACK_TAURI_STORE)
.expect("Store should already be loaded or created");
match store
.get(HAYSTACK_SCREENSHOT_SHORTCUT)
.expect("Screenshot shortcut should already be stored")
{
JsonValue::String(str) => str,
unexpected_type => panic!(
"Haystack shortcuts should be stored as strings, found type: {} ",
unexpected_type
),
}
}

View File

@ -1,13 +1,7 @@
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { createEffect } from "solid-js";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { createEffect, createSignal } from "solid-js";
import { sendImage } from "../network"; import { sendImage } from "../network";
// TODO: This component should focus the window and show preview of screenshot,
// before we send it to backend, potentially we could draw and annotate
// OR we kill this and do stuff siltently
// anyhow keeping it like this for now
export function ImageViewer() { export function ImageViewer() {
// const [latestImage, setLatestImage] = createSignal<string | null>(null); // const [latestImage, setLatestImage] = createSignal<string | null>(null);
@ -17,15 +11,10 @@ export function ImageViewer() {
console.log("Received processed PNG", event); console.log("Received processed PNG", event);
const base64Data = event.payload as string; const base64Data = event.payload as string;
const appWindow = getCurrentWindow();
appWindow.show();
appWindow.setFocus();
// setLatestImage(`data:image/png;base64,${base64Data}`); // setLatestImage(`data:image/png;base64,${base64Data}`);
const result = await sendImage("test-image.png", base64Data); const result = await sendImage("test-image.png", base64Data);
window.location.reload();
console.log("DBG: ", result); console.log("DBG: ", result);
}); });
@ -37,7 +26,9 @@ export function ImageViewer() {
return null; return null;
// return ( // return (
// <div class="fixed inset-0"> // <div>
// <FolderPicker />
// {latestImage() && ( // {latestImage() && (
// <div class="mt-4"> // <div class="mt-4">
// <h3>Latest Processed Image:</h3> // <h3>Latest Processed Image:</h3>