16 Commits

Author SHA1 Message Date
9407f54677 fix: mobile entry point cfg_attr + java sdk versions 2025-04-26 11:44:02 +01:00
2bcf06f5c6 feat(share-target): seperate component to handle sharing 2025-04-26 11:44:02 +01:00
ecd59ec814 feat: bringing back shortcuts 2025-04-26 11:44:02 +01:00
0ef20264c1 chore(platforms): seperating permissions and inits on different platforms 2025-04-26 11:43:57 +01:00
0c2c8bde74 fix: backend err 2025-04-26 11:43:37 +01:00
bb4760036e feat: sending image to the backend 2025-04-26 11:43:37 +01:00
3e8df1ba6f feat(images): share target working and receiving images! 2025-04-26 11:43:37 +01:00
1c265d8a60 feat: working android dev environment 2025-04-26 11:43:10 +01:00
4f2b78b9f1 wip: working release sdk 2025-04-26 11:43:00 +01:00
d935d6a8b9 wip: sorting out various versioning problems 2025-04-26 11:43:00 +01:00
03656cf42e BIGWIP(android): trying to make an android release
fucking stupid shit why is it so hard
2025-04-26 11:43:00 +01:00
e735aca168 refactor(image-viewer): update ImageViewer component structure and remove commented code 2025-04-22 22:13:40 +02:00
8ea0d53af7 feat(screenshot): implement area screenshot functionality and integrate with shortcut management 2025-04-22 22:04:26 +02:00
8850b00595 feat(shortcuts): add screenshot shortcut management and registration 2025-04-22 21:36:37 +02:00
e0fadb2c66 feat(shortcuts): improve global shortcut management and documentation 2025-04-22 21:00:48 +02:00
c3fc915e60 feat(image-viewer): enhance window focus and visibility handling in ImageViewer component 2025-04-22 20:56:08 +02:00
10 changed files with 350 additions and 64 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,38 @@
---
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.

53
frontend/.idea/workspace.xml generated Normal file
View File

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

View File

@ -26,6 +26,9 @@ base64 = "0.21.7"
tokio = { version = "1.36.0", features = ["full"] }
tauri-plugin-store = "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-dialog = "2.2.1"
tauri-plugin-opener = "2.2.6"

View File

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

View File

@ -0,0 +1,46 @@
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,3 +1,4 @@
use crate::screenshot::take_area_screenshot;
use tauri::App;
use tauri::AppHandle;
use tauri::Emitter;
@ -9,28 +10,36 @@ use tauri_plugin_global_shortcut::ShortcutState;
use tauri_plugin_store::JsonValue;
use tauri_plugin_store::StoreExt;
/// Name of the Tauri storage
const HAYSTACK_TAURI_STORE: &str = "haystack_tauri_store";
/// Constants for Tauri store configuration
const HAYSTACK_TAURI_STORE: &str = "haystack_tauri_store"; // Name of the persistent 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
/// Key for storing global shortcuts
const HAYSTACK_GLOBAL_SHORTCUT: &str = "haystack_global_shortcut";
/// Default shortcut for macOS
/// Platform-specific default shortcuts
#[cfg(target_os = "macos")]
const DEFAULT_SHORTCUT: &str = "command+shift+k";
const DEFAULT_SHORTCUT: &str = "command+shift+k"; // macOS uses Command key
#[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"))]
const DEFAULT_SHORTCUT: &str = "ctrl+shift+k";
const DEFAULT_SHORTCUT: &str = "ctrl+shift+k"; // Windows/Linux use Ctrl key
#[cfg(any(target_os = "windows", target_os = "linux"))]
const DEFAULT_SCREENSHOT_SHORTCUT: &str = "ctrl+shift+p"; // Windows/Linux screenshot shortcut
/// Set shortcut during application startup
/// Initializes the global 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) {
// Get or create the persistent store
let store = app
.store(HAYSTACK_TAURI_STORE)
.expect("Creating the store should not fail");
// Use stored shortcut if it exists
if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) {
// Initialize toggle window shortcut
let toggle_shortcut = if let Some(stored_shortcut) = store.get(HAYSTACK_GLOBAL_SHORTCUT) {
let stored_shortcut_str = match stored_shortcut {
JsonValue::String(str) => str,
unexpected_type => panic!(
@ -38,45 +47,72 @@ pub fn enable_shortcut(app: &App) {
unexpected_type
),
};
let stored_shortcut = stored_shortcut_str
stored_shortcut_str
.parse::<Shortcut>()
.expect("Stored shortcut string should be valid");
_register_shortcut_upon_start(app, stored_shortcut); // Register stored shortcut
.expect("Stored shortcut string should be valid")
} else {
// Use default shortcut if none is stored
store.set(
HAYSTACK_GLOBAL_SHORTCUT,
JsonValue::String(DEFAULT_SHORTCUT.to_string()),
);
let default_shortcut = DEFAULT_SHORTCUT
DEFAULT_SHORTCUT
.parse::<Shortcut>()
.expect("Default shortcut should be valid");
_register_shortcut_upon_start(app, default_shortcut); // Register default shortcut
}
.expect("Default shortcut should be valid")
};
// 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);
}
/// Get the current stored shortcut as a string
/// Returns the currently configured shortcut as a string.
/// This is exposed as a Tauri command for the frontend to query.
#[tauri::command]
pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
let shortcut = _get_shortcut(&app);
Ok(shortcut)
Ok(get_shortcut_from_store(&app))
}
/// Unregister the current shortcut in Tauri
/// Unregisters the current global shortcut from the system.
/// This is exposed as a Tauri command for the frontend to trigger.
#[tauri::command]
pub fn unregister_shortcut<R: Runtime>(app: AppHandle<R>) {
let shortcut_str = _get_shortcut(&app);
let shortcut_str = get_shortcut_from_store(&app);
let shortcut = shortcut_str
.parse::<Shortcut>()
.expect("Stored shortcut string should be valid");
// Unregister the shortcut
app.global_shortcut()
.unregister(shortcut)
.expect("Failed to unregister shortcut")
}
/// Change the global shortcut
/// Changes the global shortcut to a new key combination.
/// This function:
/// 1. Validates the new shortcut
/// 2. Stores it in the persistent store
/// 3. Registers it with the system
#[tauri::command]
pub fn change_shortcut<R: Runtime>(
app: AppHandle<R>,
@ -96,28 +132,40 @@ pub fn change_shortcut<R: Runtime>(
store.set(HAYSTACK_GLOBAL_SHORTCUT, JsonValue::String(key));
// Register the new shortcut
_register_shortcut(&app, shortcut);
register_shortcut(&app, shortcut);
Ok(())
}
/// Helper function to register a shortcut, primarily for updating shortcuts
fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
/// Handles the window visibility toggle logic when the shortcut is pressed.
/// This function:
/// 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();
// Register global shortcut and define its behavior
app.global_shortcut()
.on_shortcut(shortcut, move |app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
// 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();
}
handle_window_visibility(app, &main_window);
}
}
})
@ -125,39 +173,49 @@ fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
.unwrap();
}
/// Helper function to register shortcuts during application startup
fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
/// Registers a shortcut during application startup.
/// This is similar to register_shortcut but handles the initial plugin setup.
/// 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
.get_webview_window("main")
.expect("webview to be defined");
// Initialize global shortcut and set its handler
app.handle()
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |app, scut, event| {
if scut == &shortcut {
if scut == &toggle_shortcut {
if let ShortcutState::Pressed = event.state() {
// 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();
handle_window_visibility(app, &window);
}
} 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(),
)
.unwrap();
app.global_shortcut().register(shortcut).unwrap(); // Register global shortcut
app.global_shortcut().register(toggle_shortcut).unwrap();
app.global_shortcut().register(screenshot_shortcut).unwrap();
}
/// Retrieve the stored global shortcut as a string
pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
/// Retrieves the currently stored shortcut from the persistent store.
/// This is a helper function used by other functions to access the stored shortcut.
fn get_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");
@ -173,3 +231,73 @@ pub fn _get_shortcut<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,7 +1,13 @@
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";
// 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() {
// const [latestImage, setLatestImage] = createSignal<string | null>(null);
@ -11,10 +17,15 @@ export function ImageViewer() {
console.log("Received processed PNG", event);
const base64Data = event.payload as string;
const appWindow = getCurrentWindow();
appWindow.show();
appWindow.setFocus();
// setLatestImage(`data:image/png;base64,${base64Data}`);
const result = await sendImage("test-image.png", base64Data);
window.location.reload();
console.log("DBG: ", result);
});
@ -26,9 +37,7 @@ export function ImageViewer() {
return null;
// return (
// <div>
// <FolderPicker />
// <div class="fixed inset-0">
// {latestImage() && (
// <div class="mt-4">
// <h3>Latest Processed Image:</h3>