Compare commits

..

15 commits

Author SHA1 Message Date
0PandaDEV
aa928f7094
chore(deps): update keyboard dependency and refactor keyboard handling 2025-05-27 14:44:14 +02:00
0PandaDEV
f37cebd594
chore(deps): update Rust version and tauri dependencies in Cargo.toml
Some checks failed
Nightly Builds / prepare (push) Successful in 5s
Nightly Builds / build-ubuntu (push) Failing after 18s
Nightly Builds / build-macos (arm64, --target aarch64-apple-darwin) (push) Has been cancelled
Nightly Builds / build-macos (x64, --target x86_64-apple-darwin) (push) Has been cancelled
Nightly Builds / build-windows (arm64, --target aarch64-pc-windows-msvc, aarch64-pc-windows-msvc) (push) Has been cancelled
Nightly Builds / build-windows (x64, --target x86_64-pc-windows-msvc, x86_64-pc-windows-msvc) (push) Has been cancelled
2025-05-27 14:08:57 +02:00
0PandaDEV
0be474c285
chore(deps): update dependencies in Cargo.lock and Cargo.toml 2025-05-27 14:06:42 +02:00
0PandaDEV
7ec9bf8ca3
chore(deps): update dependencies and add bun.lock file 2025-05-27 14:02:43 +02:00
0PandaDEV
5fd0485aa7
refactor: replace wrdu-keyboard with @waradu/keyboard for keyboard handling 2025-05-27 14:02:14 +02:00
PandaDEV
2c4459f340
Merge branch 'main' into dev/actions-menu 2025-05-27 13:29:44 +02:00
PandaDEV
95a4ad624a
Update FUNDING.yml
Some checks failed
Nightly Builds / prepare (push) Successful in 1m3s
Nightly Builds / build-ubuntu (push) Failing after 1m4s
Nightly Builds / build-macos (arm64, --target aarch64-apple-darwin) (push) Has been cancelled
Nightly Builds / build-macos (x64, --target x86_64-apple-darwin) (push) Has been cancelled
Nightly Builds / build-windows (arm64, --target aarch64-pc-windows-msvc, aarch64-pc-windows-msvc) (push) Has been cancelled
Nightly Builds / build-windows (x64, --target x86_64-pc-windows-msvc, x86_64-pc-windows-msvc) (push) Has been cancelled
2025-05-17 12:54:57 +02:00
PandaDEV
bbf4ae33e0
docs(readme): wrong download links
Some checks failed
Nightly Builds / prepare (push) Successful in 59s
Nightly Builds / build-ubuntu (push) Failing after 1m13s
Nightly Builds / build-macos (arm64, --target aarch64-apple-darwin) (push) Has been cancelled
Nightly Builds / build-macos (x64, --target x86_64-apple-darwin) (push) Has been cancelled
Nightly Builds / build-windows (arm64, --target aarch64-pc-windows-msvc, aarch64-pc-windows-msvc) (push) Has been cancelled
Nightly Builds / build-windows (x64, --target x86_64-pc-windows-msvc, x86_64-pc-windows-msvc) (push) Has been cancelled
2025-04-26 19:13:53 +02:00
PandaDEV
8adbeb1c6e
Update README.md
Some checks failed
Nightly Builds / prepare (push) Has been cancelled
Nightly Builds / build-macos (arm64, --target aarch64-apple-darwin) (push) Has been cancelled
Nightly Builds / build-macos (x64, --target x86_64-apple-darwin) (push) Has been cancelled
Nightly Builds / build-windows (arm64, --target aarch64-pc-windows-msvc, aarch64-pc-windows-msvc) (push) Has been cancelled
Nightly Builds / build-windows (x64, --target x86_64-pc-windows-msvc, x86_64-pc-windows-msvc) (push) Has been cancelled
Nightly Builds / build-ubuntu (push) Has been cancelled
2025-04-15 20:42:15 +02:00
PandaDEV
1a83671927
Merge pull request #38 from 0PandaDEV/dependabot/cargo/src-tauri/ring-0.17.14
chore(deps): bump ring from 0.17.8 to 0.17.14 in /src-tauri
2025-03-17 23:01:03 +01:00
PandaDEV
e7964d2eba
Merge pull request #39 from 0PandaDEV/dependabot/cargo/src-tauri/openssl-0.10.71
chore(deps): bump openssl from 0.10.66 to 0.10.71 in /src-tauri
2025-03-17 23:00:49 +01:00
PandaDEV
ebf7f55433
Merge pull request #37 from 0PandaDEV/dependabot/cargo/src-tauri/zip-2.4.1
chore(deps): bump zip from 2.1.6 to 2.4.1 in /src-tauri
2025-03-17 23:00:36 +01:00
dependabot[bot]
987cb478dc
chore(deps): bump openssl from 0.10.66 to 0.10.71 in /src-tauri
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.66 to 0.10.71.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.66...openssl-v0.10.71)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 22:00:36 +00:00
dependabot[bot]
cad31b666c
chore(deps): bump ring from 0.17.8 to 0.17.14 in /src-tauri
Bumps [ring](https://github.com/briansmith/ring) from 0.17.8 to 0.17.14.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 22:00:14 +00:00
dependabot[bot]
2c0dba0e66
chore(deps): bump zip from 2.1.6 to 2.4.1 in /src-tauri
Bumps [zip](https://github.com/zip-rs/zip2) from 2.1.6 to 2.4.1.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v2.1.6...v2.4.1)

---
updated-dependencies:
- dependency-name: zip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 21:59:57 +00:00
8 changed files with 365 additions and 531 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,3 +1 @@
github: 0pandadev github: 0pandadev
buy_me_a_coffee: pandadev_
ko_fi: pandadev_

View file

@ -5,31 +5,31 @@
The fixed and simple clipboard manager for both Windows and Linux. The fixed and simple clipboard manager for both Windows and Linux.
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_x64.msi"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_x64.msi">
<img src="./public/windows.png"> Windows (x64) <img src="./public/windows.png"> Windows (x64)
</a> </a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_arm64.msi"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_arm64.msi">
Windows (arm64) Windows (arm64)
</a> </a>
<br> <br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.deb"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.deb">
<img src="./public/linux.png"> Linux (deb) <img src="./public/linux.png"> Linux (deb)
</a> </a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.rpm"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.rpm">
Linux (rpm) Linux (rpm)
</a> </a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1.AppImage"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3.AppImage">
Linux (AppImage) Linux (AppImage)
</a> </a>
<br> <br>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_silicon.dmg"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_silicon.dmg">
<img src="./public/apple.png"> macOS (Silicon) <img src="./public/apple.png"> macOS (Silicon)
</a> </a>
<a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.1/Qopy-0.3.1_intel.dmg"> <a href="https://github.com/0PandaDEV/Qopy/releases/download/v0.3.3/Qopy-0.3.3_intel.dmg">
macOS (Intel) macOS (Intel)
</a> </a>
<br> <br>
@ -123,7 +123,7 @@ bun build
## 📝 License ## 📝 License
Qopy is licensed under GPL-3. See the [LICENSE file](./LICENCE) for more information. Qopy is licensed under AGPL-3. See the [LICENSE file](./LICENCE) for more information.
[codespaces-link]: https://codespaces.new/0pandadev/Qopy [codespaces-link]: https://codespaces.new/0pandadev/Qopy
[codespaces-shield]: https://github.com/codespaces/badge.svg [codespaces-shield]: https://github.com/codespaces/badge.svg

View file

@ -20,7 +20,7 @@
"sass-embedded": "1.85.1", "sass-embedded": "1.85.1",
"uuid": "11.1.0", "uuid": "11.1.0",
"vue": "3.5.13", "vue": "3.5.13",
"wrdu-keyboard": "github:0PandaDEV/keyboard" "@waradu/keyboard": "4.2.0"
}, },
"overrides": { "overrides": {
"chokidar": "^3.6.0" "chokidar": "^3.6.0"

View file

@ -1,41 +1,68 @@
<template> <template>
<main> <main>
<TopBar ref="topBar" @search="searchHistory" @searchStarted="searchStarted" /> <TopBar
ref="topBar"
@search="searchHistory"
@searchStarted="searchStarted" />
<div class="container"> <div class="container">
<OverlayScrollbarsComponent class="results" ref="resultsContainer" <OverlayScrollbarsComponent
class="results"
ref="resultsContainer"
:options="{ scrollbars: { autoHide: 'scroll' } }"> :options="{ scrollbars: { autoHide: 'scroll' } }">
<div v-for="(group, groupIndex) in groupedHistory" :key="groupIndex" class="group"> <div
v-for="(group, groupIndex) in groupedHistory"
:key="groupIndex"
class="group">
<div class="time-separator">{{ group.label }}</div> <div class="time-separator">{{ group.label }}</div>
<div class="results-group"> <div class="results-group">
<Result v-for="(item, index) in group.items" :key="item.id" :item="item" <Result
:selected="isSelected(groupIndex, index)" :image-url="imageUrls[item.id]" v-for="(item, index) in group.items"
:dimensions="imageDimensions[item.id]" @select="selectItem(groupIndex, index)" @image-error="onImageError" :key="item.id"
:item="item"
:selected="isSelected(groupIndex, index)"
:image-url="imageUrls[item.id]"
:dimensions="imageDimensions[item.id]"
@select="selectItem(groupIndex, index)"
@image-error="onImageError"
@setRef="(el: HTMLElement | null) => (selectedElement = el)" /> @setRef="(el: HTMLElement | null) => (selectedElement = el)" />
</div> </div>
</div> </div>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
<div class="right"> <div class="right">
<div class="content" v-if="selectedItem?.content_type === ContentType.Image"> <div
class="content"
v-if="selectedItem?.content_type === ContentType.Image">
<img :src="imageUrls[selectedItem.id]" alt="Image" class="image" /> <img :src="imageUrls[selectedItem.id]" alt="Image" class="image" />
</div> </div>
<div v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)" class="content"> <div
<img class="image" :src="getYoutubeThumbnail(selectedItem.content)" alt="YouTube Thumbnail" /> v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)"
class="content">
<img
class="image"
:src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail" />
</div> </div>
<div class="content" v-else-if=" <div
selectedItem?.content_type === ContentType.Link && pageOgImage class="content"
"> v-else-if="
selectedItem?.content_type === ContentType.Link && pageOgImage
">
<img :src="pageOgImage" alt="Image" class="image" /> <img :src="pageOgImage" alt="Image" class="image" />
</div> </div>
<OverlayScrollbarsComponent v-else class="content"> <OverlayScrollbarsComponent v-else class="content">
<span class="content-text">{{ selectedItem?.content || "" }}</span> <span class="content-text">{{ selectedItem?.content || "" }}</span>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
<OverlayScrollbarsComponent class="information" :options="{ scrollbars: { autoHide: 'scroll' } }"> <OverlayScrollbarsComponent
class="information"
:options="{ scrollbars: { autoHide: 'scroll' } }">
<div class="title">Information</div> <div class="title">Information</div>
<div class="info-content" v-if="selectedItem && getInfo"> <div class="info-content" v-if="selectedItem && getInfo">
<div class="info-row" v-for="(row, index) in infoRows" :key="index"> <div class="info-row" v-for="(row, index) in infoRows" :key="index">
<p class="label">{{ row.label }}</p> <p class="label">{{ row.label }}</p>
<span :class="{ 'url-truncate': row.isUrl }" :data-text="row.value"> <span
<img v-if="row.icon" :src="row.icon" :alt="String(row.value)"> :class="{ 'url-truncate': row.isUrl }"
:data-text="row.value">
<img v-if="row.icon" :src="row.icon" :alt="String(row.value)" />
{{ row.value }} {{ row.value }}
</span> </span>
</div> </div>
@ -43,26 +70,39 @@
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
</div> </div>
</div> </div>
<BottomBar :primary-action="{ <BottomBar
text: 'Paste', :primary-action="{
icon: IconsEnter, text: 'Paste',
onClick: pasteSelectedItem, icon: IconsEnter,
}" :secondary-action="{ onClick: pasteSelectedItem,
text: 'Actions', }"
icon: IconsKey, :secondary-action="{
input: 'K', text: 'Actions',
showModifier: true, icon: IconsKey,
onClick: toggleActionsMenu, input: 'K',
}" /> showModifier: true,
<ActionsMenu :selected-item="selectedItem" :is-visible="isActionsMenuVisible" @close="closeActionsMenu" @toggle="toggleActionsMenu" /> onClick: toggleActionsMenu,
}" />
<ActionsMenu
:selected-item="selectedItem"
:is-visible="isActionsMenuVisible"
@close="closeActionsMenu"
@toggle="toggleActionsMenu" />
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick, shallowRef } from "vue"; import {
ref,
computed,
onMounted,
onUnmounted,
watch,
nextTick,
shallowRef,
} from "vue";
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"; import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import "overlayscrollbars/overlayscrollbars.css"; import "overlayscrollbars/overlayscrollbars.css";
import { platform } from "@tauri-apps/plugin-os";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { useNuxtApp } from "#app"; import { useNuxtApp } from "#app";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
@ -86,7 +126,14 @@ interface GroupedHistory {
} }
const { $history, $keyboard, $selectedResult } = useNuxtApp(); const { $history, $keyboard, $selectedResult } = useNuxtApp();
const { selectedGroupIndex, selectedItemIndex, selectedElement, useSelectedResult } = $selectedResult; const {
selectedGroupIndex,
selectedItemIndex,
selectedElement,
useSelectedResult,
} = $selectedResult;
const listeners: Array<() => void> = [];
const CHUNK_SIZE = 50; const CHUNK_SIZE = 50;
const SCROLL_THRESHOLD = 100; const SCROLL_THRESHOLD = 100;
@ -102,7 +149,6 @@ const resultsContainer = shallowRef<InstanceType<
> | null>(null); > | null>(null);
const searchQuery = ref(""); const searchQuery = ref("");
const searchInput = ref<HTMLInputElement | null>(null); const searchInput = ref<HTMLInputElement | null>(null);
const os = ref<string>("");
const imageUrls = shallowRef<Record<string, string>>({}); const imageUrls = shallowRef<Record<string, string>>({});
const imageDimensions = shallowRef<Record<string, string>>({}); const imageDimensions = shallowRef<Record<string, string>>({});
const imageSizes = shallowRef<Record<string, string>>({}); const imageSizes = shallowRef<Record<string, string>>({});
@ -117,18 +163,9 @@ const topBar = ref<{ searchInput: HTMLInputElement | null } | null>(null);
const toggleActionsMenu = () => { const toggleActionsMenu = () => {
isActionsMenuVisible.value = !isActionsMenuVisible.value; isActionsMenuVisible.value = !isActionsMenuVisible.value;
if (isActionsMenuVisible.value) {
$keyboard.disableContext('main');
$keyboard.enableContext('actionsMenu');
} else {
$keyboard.disableContext('actionsMenu');
$keyboard.enableContext('main');
}
nextTick(() => { nextTick(() => {
if (isActionsMenuVisible.value) { if (isActionsMenuVisible.value) {
document.getElementById('actions-menu')?.focus(); document.getElementById("actions-menu")?.focus();
} else { } else {
focusSearchInput(); focusSearchInput();
} }
@ -137,8 +174,7 @@ const toggleActionsMenu = () => {
const closeActionsMenu = () => { const closeActionsMenu = () => {
isActionsMenuVisible.value = false; isActionsMenuVisible.value = false;
$keyboard.disableContext('actionsMenu'); focusSearchInput();
$keyboard.enableContext('main');
}; };
const isSameDay = (date1: Date, date2: Date): boolean => { const isSameDay = (date1: Date, date2: Date): boolean => {
@ -155,7 +191,7 @@ const getWeekNumber = (date: Date): number => {
((date.getTime() - firstDayOfYear.getTime()) / 86400000 + ((date.getTime() - firstDayOfYear.getTime()) / 86400000 +
firstDayOfYear.getDay() + firstDayOfYear.getDay() +
1) / 1) /
7 7
); );
}; };
@ -176,8 +212,8 @@ const groupedHistory = computed<GroupedHistory[]>(() => {
const filteredItems = searchQuery.value const filteredItems = searchQuery.value
? history.value.filter((item) => ? history.value.filter((item) =>
item.content.toLowerCase().includes(searchQuery.value.toLowerCase()) item.content.toLowerCase().includes(searchQuery.value.toLowerCase())
) )
: history.value; : history.value;
const yesterday = new Date(today.getTime() - 86400000); const yesterday = new Date(today.getTime() - 86400000);
@ -243,16 +279,16 @@ const loadHistoryChunk = async (): Promise<void> => {
img.src = `data:image/png;base64,${base64}`; img.src = `data:image/png;base64,${base64}`;
imageUrls.value[historyItem.id] = img.src; imageUrls.value[historyItem.id] = img.src;
await new Promise<void>((resolve) => { await new Promise<void>((resolveProm) => {
img.onload = () => { img.onload = () => {
imageDimensions.value[ imageDimensions.value[
historyItem.id historyItem.id
] = `${img.width}x${img.height}`; ] = `${img.width}x${img.height}`;
resolve(); resolveProm();
}; };
img.onerror = () => { img.onerror = () => {
imageDimensions.value[historyItem.id] = "Error"; imageDimensions.value[historyItem.id] = "Error";
resolve(); resolveProm();
}; };
}); });
} catch (error) { } catch (error) {
@ -366,7 +402,11 @@ const processSearchQueue = async () => {
{ id: item.id, timestamp: new Date(item.timestamp) } { id: item.id, timestamp: new Date(item.timestamp) }
) )
); );
if (groupedHistory.value.length > 0) {
handleSelection(0, 0, true);
} else {
selectItem(-1, -1);
}
} catch (error) { } catch (error) {
console.error("Search error:", error); console.error("Search error:", error);
} finally { } finally {
@ -394,10 +434,14 @@ const searchHistory = async (query: string): Promise<void> => {
watch( watch(
() => groupedHistory.value, () => groupedHistory.value,
(newGroupedHistory) => { (newGroupedHistory, oldGroupedHistory) => {
if (newGroupedHistory.length > 0) { if (
newGroupedHistory.length > 0 &&
oldGroupedHistory &&
oldGroupedHistory.length === 0
) {
handleSelection(0, 0, true); handleSelection(0, 0, true);
} else { } else if (newGroupedHistory.length === 0) {
selectItem(-1, -1); selectItem(-1, -1);
} }
}, },
@ -474,16 +518,16 @@ const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
img.src = `data:image/png;base64,${base64}`; img.src = `data:image/png;base64,${base64}`;
imageUrls.value[historyItem.id] = img.src; imageUrls.value[historyItem.id] = img.src;
await new Promise<void>((resolve) => { await new Promise<void>((resolveProm) => {
img.onload = () => { img.onload = () => {
imageDimensions.value[ imageDimensions.value[
historyItem.id historyItem.id
] = `${img.width}x${img.height}`; ] = `${img.width}x${img.height}`;
resolve(); resolveProm();
}; };
img.onerror = () => { img.onerror = () => {
imageDimensions.value[historyItem.id] = "Error"; imageDimensions.value[historyItem.id] = "Error";
resolve(); resolveProm();
}; };
}); });
} catch (error) { } catch (error) {
@ -520,72 +564,6 @@ const handleSelection = (
if (shouldScroll) scrollToSelectedItem(); if (shouldScroll) scrollToSelectedItem();
}; };
const setupEventListeners = async (): Promise<void> => {
await listen("clipboard-content-updated", async () => {
lastUpdateTime.value = Date.now();
await updateHistory(true);
if (groupedHistory.value[0]?.items.length > 0) {
handleSelection(0, 0, false);
}
});
await listen("tauri://focus", async () => {
const currentTime = Date.now();
if (currentTime - lastUpdateTime.value > 0) {
const previousState = {
groupIndex: selectedGroupIndex.value,
itemIndex: selectedItemIndex.value,
scroll:
resultsContainer.value?.osInstance()?.elements().viewport
?.scrollTop || 0,
};
await updateHistory();
lastUpdateTime.value = currentTime;
handleSelection(previousState.groupIndex, previousState.itemIndex, false);
if (resultsContainer.value?.osInstance()?.elements().viewport?.scrollTo) {
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
top: previousState.scroll,
behavior: "instant",
});
}
}
focusSearchInput();
$keyboard.disableContext('actionsMenu');
$keyboard.disableContext('settings');
$keyboard.enableContext('main');
if (isActionsMenuVisible.value) {
$keyboard.enableContext('actionsMenu');
}
});
await listen("tauri://blur", () => {
searchInput.value?.blur();
$keyboard.disableContext('main');
$keyboard.disableContext('actionsMenu');
});
$keyboard.setupAppShortcuts({
onNavigateDown: selectNext,
onNavigateUp: selectPrevious,
onSelect: pasteSelectedItem,
onEscape: () => {
if (isActionsMenuVisible.value) {
closeActionsMenu();
} else {
hideApp();
}
},
onToggleActions: toggleActionsMenu,
contextName: 'main',
priority: $keyboard.PRIORITY.HIGH
});
$keyboard.disableContext('settings');
$keyboard.enableContext('main');
};
const { hideApp } = useAppControl(); const { hideApp } = useAppControl();
const focusSearchInput = (): void => { const focusSearchInput = (): void => {
@ -609,38 +587,147 @@ watch(
onMounted(async () => { onMounted(async () => {
try { try {
os.value = platform();
await loadHistoryChunk(); await loadHistoryChunk();
if (groupedHistory.value.length > 0 && !selectedItem.value) {
handleSelection(0, 0, true);
}
resultsContainer.value resultsContainer.value
?.osInstance() ?.osInstance()
?.elements() ?.elements()
?.viewport?.addEventListener("scroll", handleScroll); ?.viewport?.addEventListener("scroll", handleScroll);
await setupEventListeners(); listeners.push(
await listen("clipboard-content-updated", async () => {
$keyboard.setupAppShortcuts({ lastUpdateTime.value = Date.now();
onNavigateDown: selectNext, await updateHistory(true);
onNavigateUp: selectPrevious, if (groupedHistory.value[0]?.items.length > 0) {
onSelect: pasteSelectedItem, handleSelection(0, 0, false);
onEscape: () => {
if (isActionsMenuVisible.value) {
closeActionsMenu();
} else {
hideApp();
} }
}, })
onToggleActions: toggleActionsMenu, );
contextName: 'main',
priority: $keyboard.PRIORITY.HIGH listeners.push(
}); await listen("tauri://focus", async () => {
$keyboard.disableContext('settings'); console.log("Tauri window focused");
$keyboard.enableContext('main'); // Attempt to re-initialize keyboard listeners
if ($keyboard && typeof $keyboard.init === "function") {
console.log("Re-initializing keyboard via $keyboard.init()");
$keyboard.init();
} else {
console.warn("$keyboard.init is not available");
}
const currentTime = Date.now();
if (currentTime - lastUpdateTime.value > 0) {
const previousState = {
groupIndex: selectedGroupIndex.value,
itemIndex: selectedItemIndex.value,
scroll:
resultsContainer.value?.osInstance()?.elements().viewport
?.scrollTop || 0,
};
await updateHistory();
lastUpdateTime.value = currentTime;
handleSelection(
previousState.groupIndex,
previousState.itemIndex,
false
);
if (
resultsContainer.value?.osInstance()?.elements().viewport?.scrollTo
) {
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
top: previousState.scroll,
behavior: "instant",
});
}
}
focusSearchInput();
})
);
listeners.push(
await listen("tauri://blur", () => {
searchInput.value?.blur();
})
);
listeners.push(
$keyboard.listen(
[$keyboard.Key.DownArrow],
() => {
console.log(
"Down Arrow pressed. Active element:",
document.activeElement
);
selectNext();
},
{ prevent: true }
)
);
listeners.push(
$keyboard.listen(
[$keyboard.Key.UpArrow],
() => {
console.log(
"Up Arrow pressed. Active element:",
document.activeElement
);
selectPrevious();
},
{ prevent: true }
)
);
listeners.push(
$keyboard.listen([$keyboard.Key.Enter], pasteSelectedItem, {
prevent: true,
})
);
listeners.push(
$keyboard.listen(
[$keyboard.Key.Escape],
() => {
if (isActionsMenuVisible.value) {
closeActionsMenu();
} else {
hideApp();
}
},
{ prevent: true }
)
);
const metaOrCtrlKey =
$keyboard.currentOS === "macos"
? $keyboard.Key.Meta
: $keyboard.Key.Control;
listeners.push(
$keyboard.listen([metaOrCtrlKey, $keyboard.Key.K], toggleActionsMenu, {
prevent: true,
ignoreIfEditable: true,
})
);
} catch (error) { } catch (error) {
console.error("Error during onMounted:", error); console.error("Error during onMounted:", error);
} }
}); });
onUnmounted(() => {
listeners.forEach((unlisten) => {
if (typeof unlisten === "function") {
unlisten();
}
});
listeners.length = 0;
const viewport = resultsContainer.value?.osInstance()?.elements()?.viewport;
if (viewport) {
viewport.removeEventListener("scroll", handleScroll);
}
});
const getFormattedDate = computed(() => { const getFormattedDate = computed(() => {
if (!selectedItem.value?.timestamp) return ""; if (!selectedItem.value?.timestamp) return "";
return new Intl.DateTimeFormat("en-US", { return new Intl.DateTimeFormat("en-US", {
@ -787,7 +874,9 @@ const infoRows = computed(() => {
label: "Source", label: "Source",
value: getInfo.value.source, value: getInfo.value.source,
isUrl: false, isUrl: false,
icon: selectedItem.value?.source_icon ? `data:image/png;base64,${selectedItem.value.source_icon}` : undefined icon: selectedItem.value?.source_icon
? `data:image/png;base64,${selectedItem.value.source_icon}`
: undefined,
}, },
{ {
label: "Content Type", label: "Content Type",
@ -800,7 +889,12 @@ const infoRows = computed(() => {
const typeSpecificRows: Record< const typeSpecificRows: Record<
ContentType, ContentType,
Array<{ label: string; value: string | number; isUrl?: boolean; icon?: string }> Array<{
label: string;
value: string | number;
isUrl?: boolean;
icon?: string;
}>
> = { > = {
[ContentType.Text]: [ [ContentType.Text]: [
{ label: "Characters", value: (getInfo.value as InfoText).characters }, { label: "Characters", value: (getInfo.value as InfoText).characters },
@ -818,7 +912,7 @@ const infoRows = computed(() => {
], ],
[ContentType.Link]: [ [ContentType.Link]: [
...((getInfo.value as InfoLink).title && ...((getInfo.value as InfoLink).title &&
(getInfo.value as InfoLink).title !== "Loading..." (getInfo.value as InfoLink).title !== "Loading..."
? [{ label: "Title", value: (getInfo.value as InfoLink).title || "" }] ? [{ label: "Title", value: (getInfo.value as InfoLink).title || "" }]
: []), : []),
{ label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true }, { label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true },

View file

@ -45,7 +45,6 @@
<div <div
@blur="onBlur" @blur="onBlur"
@focus="onFocus" @focus="onFocus"
@keydown="onKeyDown"
class="keybind-input" class="keybind-input"
ref="keybindInput" ref="keybindInput"
tabindex="0" tabindex="0"
@ -75,7 +74,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from "vue"; import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { platform } from "@tauri-apps/plugin-os"; import { platform } from "@tauri-apps/plugin-os";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { KeyValues, KeyLabels } from "../types/keys"; import { KeyValues, KeyLabels } from "../types/keys";
@ -88,14 +87,14 @@ const activeModifiers = reactive<Set<KeyValues>>(new Set());
const isKeybindInputFocused = ref(false); const isKeybindInputFocused = ref(false);
const keybind = ref<KeyValues[]>([]); const keybind = ref<KeyValues[]>([]);
const keybindInput = ref<HTMLElement | null>(null); const keybindInput = ref<HTMLElement | null>(null);
const lastBlurTime = ref(0);
const blurredByEscape = ref(false); const blurredByEscape = ref(false);
const os = ref("");
const router = useRouter(); const router = useRouter();
const showEmptyKeybindError = ref(false); const showEmptyKeybindError = ref(false);
const autostart = ref(false); const autostart = ref(false);
const { $settings, $keyboard } = useNuxtApp(); const { $settings, $keyboard } = useNuxtApp();
const listeners: Array<() => void> = [];
const modifierKeySet = new Set([ const modifierKeySet = new Set([
KeyValues.AltLeft, KeyValues.AltLeft,
KeyValues.AltRight, KeyValues.AltRight,
@ -115,15 +114,15 @@ const keyToLabel = (key: KeyValues): string => {
return KeyLabels[key] || key; return KeyLabels[key] || key;
}; };
const updateKeybind = () => { const updateKeybindDisplay = () => {
const modifiers = Array.from(activeModifiers); const modifiers = Array.from(activeModifiers);
const nonModifiers = keybind.value.filter((key) => !isModifier(key)); const nonModifiers = keybind.value.filter((key) => !isModifier(key));
keybind.value = [...modifiers, ...nonModifiers]; const sortedModifiers = modifiers.sort();
keybind.value = [...sortedModifiers, ...nonModifiers];
}; };
const onBlur = () => { const onBlur = () => {
isKeybindInputFocused.value = false; isKeybindInputFocused.value = false;
lastBlurTime.value = Date.now();
showEmptyKeybindError.value = false; showEmptyKeybindError.value = false;
}; };
@ -133,35 +132,39 @@ const onFocus = () => {
activeModifiers.clear(); activeModifiers.clear();
keybind.value = []; keybind.value = [];
showEmptyKeybindError.value = false; showEmptyKeybindError.value = false;
};
const onKeyDown = (event: KeyboardEvent) => { const unlistenAll = $keyboard.listen([$keyboard.Key.All], (event: KeyboardEvent) => {
const key = event.code as KeyValues; event.preventDefault();
event.stopPropagation();
const key = event.code as KeyValues;
if (key === KeyValues.Escape) { if (key === KeyValues.Escape) {
if (keybindInput.value) {
blurredByEscape.value = true; blurredByEscape.value = true;
keybindInput.value.blur(); keybindInput.value?.blur();
event.preventDefault(); return;
event.stopPropagation();
} }
return;
}
if (isModifier(key)) { if (isModifier(key)) {
activeModifiers.add(key); activeModifiers.add(key);
} else if (!keybind.value.includes(key)) { } else {
keybind.value = keybind.value.filter((k) => isModifier(k)); const nonModifierKey = keybind.value.find(k => !isModifier(k));
keybind.value.push(key); if (!nonModifierKey || nonModifierKey === key) {
} keybind.value = Array.from(activeModifiers);
if (nonModifierKey !== key) keybind.value.push(key);
updateKeybind(); } else {
showEmptyKeybindError.value = false; keybind.value = [ ...Array.from(activeModifiers), key];
}
}
updateKeybindDisplay();
showEmptyKeybindError.value = false;
}, { prevent: true });
listeners.push(unlistenAll);
}; };
const saveKeybind = async () => { const saveKeybind = async () => {
if (keybind.value.length > 0) { const finalKeybind = keybind.value.filter(k => k);
await $settings.saveSetting("keybind", JSON.stringify(keybind.value)); if (finalKeybind.length > 0) {
await $settings.saveSetting("keybind", JSON.stringify(finalKeybind));
router.push("/"); router.push("/");
} else { } else {
showEmptyKeybindError.value = true; showEmptyKeybindError.value = true;
@ -177,67 +180,27 @@ const toggleAutostart = async () => {
await $settings.saveSetting("autostart", autostart.value ? "true" : "false"); await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
}; };
os.value = platform();
onMounted(async () => { onMounted(async () => {
$keyboard.setupKeybindCapture({ autostart.value = (await $settings.getSetting("autostart")) === "true";
onCapture: (key: string) => {
if (isKeybindInputFocused.value) {
const keyValue = key as KeyValues;
if (isModifier(keyValue)) { const metaOrCtrlKey = $keyboard.currentOS === "macos" ? $keyboard.Key.Meta : $keyboard.Key.Control;
activeModifiers.add(keyValue); listeners.push(
} else if (!keybind.value.includes(keyValue)) { $keyboard.listen([metaOrCtrlKey, $keyboard.Key.Enter], saveKeybind, { prevent: true, ignoreIfEditable: true })
keybind.value = keybind.value.filter((k) => isModifier(k)); );
keybind.value.push(keyValue);
}
updateKeybind(); listeners.push(
showEmptyKeybindError.value = false; $keyboard.listen([$keyboard.Key.Escape], () => {
} if (!isKeybindInputFocused.value && !blurredByEscape.value) {
},
onComplete: () => {
if (isKeybindInputFocused.value) {
keybindInput.value?.blur();
} else {
router.push("/"); router.push("/");
} }
} if(blurredByEscape.value) blurredByEscape.value = false;
}); }, { prevent: true })
);
if (os.value === "macos") {
$keyboard.on("settings", [$keyboard.Key.LeftMeta, $keyboard.Key.Enter], () => {
saveKeybind();
}, { priority: $keyboard.PRIORITY.HIGH });
$keyboard.on("settings", [$keyboard.Key.RightMeta, $keyboard.Key.Enter], () => {
saveKeybind();
}, { priority: $keyboard.PRIORITY.HIGH });
} else {
$keyboard.on("settings", [$keyboard.Key.LeftControl, $keyboard.Key.Enter], () => {
saveKeybind();
}, { priority: $keyboard.PRIORITY.HIGH });
$keyboard.on("settings", [$keyboard.Key.RightControl, $keyboard.Key.Enter], () => {
saveKeybind();
}, { priority: $keyboard.PRIORITY.HIGH });
}
$keyboard.on("settings", [$keyboard.Key.Escape], () => {
if (!isKeybindInputFocused.value && !blurredByEscape.value) {
router.push("/");
}
blurredByEscape.value = false;
}, { priority: $keyboard.PRIORITY.HIGH });
$keyboard.disableContext("main");
$keyboard.enableContext("settings");
autostart.value = (await $settings.getSetting("autostart")) === "true";
}); });
onUnmounted(() => { onUnmounted(() => {
$keyboard.disableContext("settings"); listeners.forEach(unlisten => unlisten());
listeners.length = 0;
}); });
</script> </script>

View file

@ -1,281 +1,27 @@
import { Key, keyboard } from "wrdu-keyboard";
import { platform } from "@tauri-apps/plugin-os"; import { platform } from "@tauri-apps/plugin-os";
import { useKeyboard, Key } from "@waradu/keyboard";
type KeyboardHandler = (event: KeyboardEvent) => void;
const activeContexts = new Set<string>();
const handlersByContext: Record<
string,
Array<{
keys: Key[];
callback: KeyboardHandler;
prevent: boolean;
priority?: number;
}>
> = {};
const PRIORITY = {
HIGH: 100,
MEDIUM: 50,
LOW: 0,
};
let currentOS = "windows";
const useKeyboard = {
PRIORITY,
registerContext: (contextName: string) => {
if (!handlersByContext[contextName]) {
handlersByContext[contextName] = [];
}
},
enableContext: (contextName: string) => {
if (!handlersByContext[contextName]) {
useKeyboard.registerContext(contextName);
}
activeContexts.add(contextName);
initKeyboardHandlers();
},
disableContext: (contextName: string) => {
activeContexts.delete(contextName);
initKeyboardHandlers();
},
on: (
contextName: string,
keys: Key[],
callback: KeyboardHandler,
options: { prevent?: boolean; priority?: number } = {}
) => {
if (!handlersByContext[contextName]) {
useKeyboard.registerContext(contextName);
}
const existingHandlerIndex = handlersByContext[contextName].findIndex(
(handler) =>
handler.keys.length === keys.length &&
handler.keys.every((key, i) => key === keys[i]) &&
handler.callback.toString() === callback.toString()
);
if (existingHandlerIndex !== -1) {
handlersByContext[contextName][existingHandlerIndex] = {
keys,
callback,
prevent: options.prevent ?? true,
priority: options.priority ?? PRIORITY.LOW,
};
} else {
handlersByContext[contextName].push({
keys,
callback,
prevent: options.prevent ?? true,
priority: options.priority ?? PRIORITY.LOW,
});
}
if (activeContexts.has(contextName)) {
initKeyboardHandlers();
}
},
clearAll: () => {
keyboard.clear();
},
setupAppShortcuts: (options: {
onNavigateUp?: () => void;
onNavigateDown?: () => void;
onSelect?: () => void;
onEscape?: () => void;
onToggleActions?: () => void;
contextName?: string;
priority?: number;
}) => {
const {
onNavigateUp,
onNavigateDown,
onSelect,
onEscape,
onToggleActions,
contextName = "app",
priority = PRIORITY.LOW,
} = options;
if (!handlersByContext[contextName]) {
useKeyboard.registerContext(contextName);
}
if (onNavigateUp) {
useKeyboard.on(contextName, [Key.UpArrow], () => onNavigateUp(), {
priority,
});
}
if (onNavigateDown) {
useKeyboard.on(contextName, [Key.DownArrow], () => onNavigateDown(), {
priority,
});
}
if (onSelect) {
useKeyboard.on(contextName, [Key.Enter], () => onSelect(), { priority });
}
if (onEscape) {
useKeyboard.on(contextName, [Key.Escape], () => onEscape(), { priority });
}
if (onToggleActions) {
const togglePriority = Math.max(priority, PRIORITY.HIGH);
if (currentOS === "macos") {
useKeyboard.on(
contextName,
[Key.LeftMeta, Key.K],
() => onToggleActions(),
{ priority: togglePriority }
);
useKeyboard.on(
contextName,
[Key.RightMeta, Key.K],
() => onToggleActions(),
{ priority: togglePriority }
);
} else {
useKeyboard.on(
contextName,
[Key.LeftControl, Key.K],
() => onToggleActions(),
{ priority: togglePriority }
);
useKeyboard.on(
contextName,
[Key.RightControl, Key.K],
() => onToggleActions(),
{ priority: togglePriority }
);
}
}
},
setupKeybindCapture: (options: {
onCapture: (key: string) => void;
onComplete: () => void;
}) => {
const { onCapture, onComplete } = options;
keyboard.prevent.down([Key.All], (event: KeyboardEvent) => {
if (event.code === "Escape") {
onComplete();
return;
}
onCapture(event.code);
});
},
};
const initKeyboardHandlers = () => {
keyboard.clear();
let allHandlers: Array<{
keys: Key[];
callback: KeyboardHandler;
prevent: boolean;
priority: number;
contextName: string;
}> = [];
for (const contextName of activeContexts) {
const handlers = handlersByContext[contextName] || [];
allHandlers = [
...allHandlers,
...handlers.map((handler) => ({
...handler,
priority: handler.priority ?? PRIORITY.LOW,
contextName,
})),
];
}
allHandlers.sort((a, b) => b.priority - a.priority);
const handlersByKeyCombination: Record<
string,
Array<(typeof allHandlers)[0]>
> = {};
allHandlers.forEach((handler) => {
const keyCombo = handler.keys.sort().join("+");
if (!handlersByKeyCombination[keyCombo]) {
handlersByKeyCombination[keyCombo] = [];
}
handlersByKeyCombination[keyCombo].push(handler);
});
Object.entries(handlersByKeyCombination).forEach(([_keyCombo, handlers]) => {
handlers.sort((a, b) => b.priority - a.priority);
const handler = handlers[0];
const wrappedCallback: KeyboardHandler = (event) => {
const isMetaCombo =
handler.keys.length > 1 &&
(handler.keys.includes(Key.LeftMeta) ||
handler.keys.includes(Key.RightMeta) ||
handler.keys.includes(Key.LeftControl) ||
handler.keys.includes(Key.RightControl));
const isNavigationKey =
event.key === "ArrowUp" ||
event.key === "ArrowDown" ||
event.key === "Enter" ||
event.key === "Escape";
const isInInput =
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement;
if (
(isMetaCombo || isNavigationKey || !isInInput) &&
activeContexts.has(handler.contextName)
) {
handler.callback(event);
}
};
if (handler.prevent) {
keyboard.prevent.down(handler.keys, wrappedCallback);
} else {
keyboard.down(handler.keys, wrappedCallback);
}
});
};
export default defineNuxtPlugin(async (nuxtApp) => { export default defineNuxtPlugin(async (nuxtApp) => {
const keyboardInstance = useKeyboard();
let currentOS = "windows";
try { try {
const osName = platform(); const osName = await Promise.resolve(platform());
currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows"; currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
} catch (error) { } catch (error) {
console.error("Error detecting platform:", error); console.error("Error detecting platform:", error);
} }
initKeyboardHandlers(); // Defer initialization until the app is mounted
nuxtApp.hook('app:mounted', () => {
nuxtApp.hook("page:finish", () => { keyboardInstance.init();
initKeyboardHandlers();
}); });
return { nuxtApp.provide('keyboard', {
provide: { listen: keyboardInstance.listen.bind(keyboardInstance),
keyboard: { init: keyboardInstance.init.bind(keyboardInstance),
...useKeyboard, Key,
Key, currentOS,
}, // Provide a clear method if users need to manually clear all listeners from the instance
}, clearAll: keyboardInstance.clear ? keyboardInstance.clear.bind(keyboardInstance) : () => { console.warn('@waradu/keyboard instance does not have a clear method'); }
}; });
}); });

46
src-tauri/Cargo.lock generated
View file

@ -105,9 +105,9 @@ dependencies = [
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
version = "1.3.2" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
dependencies = [ dependencies = [
"derive_arbitrary", "derive_arbitrary",
] ]
@ -628,12 +628,13 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.8" version = "1.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
"shlex",
] ]
[[package]] [[package]]
@ -998,9 +999,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "crunchy" name = "crunchy"
@ -1124,9 +1125,9 @@ dependencies = [
[[package]] [[package]]
name = "derive_arbitrary" name = "derive_arbitrary"
version = "1.3.2" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2619,7 +2620,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [ dependencies = [
"hermit-abi 0.5.0", "hermit-abi 0.5.0",
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -2841,7 +2842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@ -3573,9 +3574,9 @@ checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.66" version = "0.10.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"cfg-if", "cfg-if",
@ -3605,9 +3606,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.103" version = "0.9.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -4560,15 +4561,14 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom 0.2.15", "getrandom 0.2.15",
"libc", "libc",
"spin",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -4971,6 +4971,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.2" version = "1.4.2"
@ -7727,9 +7733,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "2.1.6" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e" checksum = "938cc23ac49778ac8340e366ddc422b2227ea176edb447e23fc0627608dddadd"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"crc32fast", "crc32fast",
@ -7737,7 +7743,7 @@ dependencies = [
"displaydoc", "displaydoc",
"indexmap 2.3.0", "indexmap 2.3.0",
"memchr", "memchr",
"thiserror 1.0.63", "thiserror 2.0.3",
] ]
[[package]] [[package]]

27
types/keyboard.d.ts vendored Normal file
View file

@ -0,0 +1,27 @@
import type { Key as WaraduKey, useKeyboard } from '@waradu/keyboard';
declare module '#app' {
interface NuxtApp {
$keyboard: {
listen: ReturnType<typeof useKeyboard>['listen'];
init: ReturnType<typeof useKeyboard>['init'];
Key: typeof WaraduKey;
currentOS: string;
clearAll: () => void;
};
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$keyboard: {
listen: ReturnType<typeof useKeyboard>['listen'];
init: ReturnType<typeof useKeyboard>['init'];
Key: typeof WaraduKey;
currentOS: string;
clearAll: () => void;
};
}
}
export {};