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
475 changes: 453 additions & 22 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ hypr-db-user = { path = "crates/db-user", package = "db-user" }
hypr-detect = { path = "crates/detect", package = "detect" }
hypr-docs = { path = "crates/docs", package = "docs" }
hypr-download-interface = { path = "crates/download-interface", package = "download-interface" }
hypr-extensions-runtime = { path = "crates/extensions-runtime", package = "extensions-runtime" }
hypr-file = { path = "crates/file", package = "file" }
hypr-gbnf = { path = "crates/gbnf", package = "gbnf" }
hypr-gguf = { path = "crates/gguf", package = "gguf" }
Expand Down Expand Up @@ -117,6 +118,7 @@ tauri-plugin-db = { path = "plugins/db" }
tauri-plugin-db2 = { path = "plugins/db2" }
tauri-plugin-deeplink2 = { path = "plugins/deeplink2" }
tauri-plugin-detect = { path = "plugins/detect" }
tauri-plugin-extensions = { path = "plugins/extensions" }
tauri-plugin-hooks = { path = "plugins/hooks" }
tauri-plugin-importer = { path = "plugins/importer" }
tauri-plugin-listener = { path = "plugins/listener" }
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ description = "Hyprnote desktop App"
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "hyprnote_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
# Note: cdylib removed because V8 (used by deno_core) has TLS relocations incompatible
# with shared libraries on Linux. Mobile support is not needed (macOS/Linux/Windows only).
crate-type = ["staticlib", "rlib"]

[build-dependencies]
tauri-build = { workspace = true, features = [] }
Expand All @@ -31,6 +33,7 @@ tauri-plugin-deep-link = { workspace = true }
tauri-plugin-deeplink2 = { workspace = true }
tauri-plugin-detect = { workspace = true }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-extensions = { workspace = true }
tauri-plugin-fs = { workspace = true }
tauri-plugin-hooks = { workspace = true }
tauri-plugin-http = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub async fn main() {
.plugin(tauri_plugin_template::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_detect::init())
.plugin(tauri_plugin_extensions::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_tray::init())
Expand Down
113 changes: 113 additions & 0 deletions apps/desktop/src/components/main/body/extensions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { PuzzleIcon, XIcon } from "lucide-react";
import { Reorder, useDragControls } from "motion/react";
import type { PointerEvent } from "react";

import { Button } from "@hypr/ui/components/ui/button";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@hypr/ui/components/ui/context-menu";
import { cn } from "@hypr/utils";

import type { Tab } from "../../../../store/zustand/tabs";
import { StandardTabWrapper } from "../index";
import { getExtensionComponent } from "./registry";

type ExtensionTab = Extract<Tab, { type: "extension" }>;

export function TabItemExtension({
tab,
tabIndex,
handleCloseThis,
handleSelectThis,
handleCloseOthers,
handleCloseAll,
}: {
tab: ExtensionTab;
tabIndex?: number;
handleCloseThis: (tab: Tab) => void;
handleSelectThis: (tab: Tab) => void;
handleCloseOthers: () => void;
handleCloseAll: () => void;
}) {
const controls = useDragControls();

return (
<ContextMenu>
<ContextMenuTrigger asChild>
<Reorder.Item
value={tab}
dragListener={false}
dragControls={controls}
as="div"
className={cn([
"h-full flex items-center gap-1 px-2 rounded-lg cursor-pointer select-none",
"hover:bg-neutral-100",
tab.active && "bg-neutral-100",
])}
onClick={() => handleSelectThis(tab)}
onPointerDown={(e: PointerEvent) => controls.start(e)}
>
<PuzzleIcon size={14} className="text-neutral-500 shrink-0" />
<span className="text-sm truncate max-w-[120px]">
{tab.extensionId}
</span>
{tabIndex && (
<span className="text-xs text-neutral-400 shrink-0">
{tabIndex}
</span>
)}
<Button
variant="ghost"
size="icon"
className="size-5 shrink-0"
onClick={(e) => {
e.stopPropagation();
handleCloseThis(tab);
}}
>
<XIcon size={12} />
</Button>
</Reorder.Item>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleCloseThis(tab)}>
Close
</ContextMenuItem>
<ContextMenuItem onClick={handleCloseOthers}>
Close Others
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleCloseAll}>Close All</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}

export function TabContentExtension({ tab }: { tab: ExtensionTab }) {
const Component = getExtensionComponent(tab.extensionId);

if (!Component) {
return (
<StandardTabWrapper>
<div className="flex items-center justify-center h-full">
<div className="text-center">
<PuzzleIcon size={48} className="mx-auto text-neutral-300 mb-4" />
<p className="text-neutral-500">
Extension not found: {tab.extensionId}
</p>
</div>
</div>
</StandardTabWrapper>
);
}

return (
<StandardTabWrapper>
<Component extensionId={tab.extensionId} state={tab.state} />
</StandardTabWrapper>
);
}
29 changes: 29 additions & 0 deletions apps/desktop/src/components/main/body/extensions/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ComponentType } from "react";

import type { ExtensionViewProps } from "../../../../types/extensions";

const extensionModules = import.meta.glob<{
default: ComponentType<ExtensionViewProps>;
}>("@extensions/*/ui.tsx", { eager: true });

export const extensionComponents: Record<
string,
ComponentType<ExtensionViewProps>
> = {};

for (const path in extensionModules) {
const mod = extensionModules[path];
const parts = path.split("/");
const extensionId = parts[parts.length - 2];
extensionComponents[extensionId] = mod.default;
}

export function getExtensionComponent(
extensionId: string,
): ComponentType<ExtensionViewProps> | undefined {
return extensionComponents[extensionId];
}

export function getAvailableExtensions(): string[] {
return Object.keys(extensionComponents);
}
16 changes: 16 additions & 0 deletions apps/desktop/src/components/main/body/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { TabContentCalendar, TabItemCalendar } from "./calendars";
import { TabContentContact, TabItemContact } from "./contacts";
import { TabContentEmpty, TabItemEmpty } from "./empty";
import { TabContentEvent, TabItemEvent } from "./events";
import { TabContentExtension, TabItemExtension } from "./extensions";
import { TabContentFolder, TabItemFolder } from "./folders";
import { TabContentHuman, TabItemHuman } from "./humans";
import { Search } from "./search";
Expand Down Expand Up @@ -285,6 +286,18 @@ function TabItem({
/>
);
}
if (tab.type === "extension") {
return (
<TabItemExtension
tab={tab}
tabIndex={tabIndex}
handleCloseThis={handleClose}
handleSelectThis={handleSelect}
handleCloseOthers={handleCloseOthers}
handleCloseAll={handleCloseAll}
/>
);
}

return null;
}
Expand All @@ -311,6 +324,9 @@ function ContentWrapper({ tab }: { tab: Tab }) {
if (tab.type === "empty") {
return <TabContentEmpty tab={tab} />;
}
if (tab.type === "extension") {
return <TabContentExtension tab={tab} />;
}

return null;
}
Expand Down
11 changes: 10 additions & 1 deletion apps/desktop/src/store/zustand/tabs/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ export const tabSchema = z.discriminatedUnion("type", [
baseTabSchema.extend({
type: z.literal("empty"),
}),
baseTabSchema.extend({
type: z.literal("extension"),
extensionId: z.string(),
state: z.record(z.string(), z.unknown()).default({}),
}),
]);

export type Tab = z.infer<typeof tabSchema>;
Expand All @@ -94,7 +99,8 @@ export type TabInput =
| { type: "organizations"; id: string }
| { type: "folders"; id: string | null }
| { type: "calendars"; month: Date }
| { type: "empty" };
| { type: "empty" }
| { type: "extension"; extensionId: string; state?: Record<string, unknown> };

export const rowIdfromTab = (tab: Tab): string => {
switch (tab.type) {
Expand All @@ -109,6 +115,7 @@ export const rowIdfromTab = (tab: Tab): string => {
case "calendars":
case "contacts":
case "empty":
case "extension":
throw new Error("invalid_resource");
case "folders":
if (!tab.id) {
Expand Down Expand Up @@ -136,6 +143,8 @@ export const uniqueIdfromTab = (tab: Tab): string => {
return `folders-${tab.id ?? "all"}`;
case "empty":
return `empty-${tab.slotId}`;
case "extension":
return `extension-${tab.extensionId}`;
}
};

Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/types/extensions.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ComponentType } from "react";

export interface ExtensionViewProps {
extensionId: string;
state?: Record<string, unknown>;
}
8 changes: 7 additions & 1 deletion apps/desktop/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,

/* Path aliases */
"baseUrl": ".",
"paths": {
"@extensions/*": ["../../extensions/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
Expand Down
9 changes: 6 additions & 3 deletions apps/desktop/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="vitest" />
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import path from "node:path";
import { defineConfig, type UserConfig } from "vite";

const host = process.env.TAURI_DEV_HOST;
Expand All @@ -12,12 +13,14 @@ export default defineConfig(() => ({
react(),
],
resolve: {
alias:
process.env.NODE_ENV === "development"
alias: {
...(process.env.NODE_ENV === "development"
? {
"@tauri-apps/plugin-updater": "/src/mocks/updater.ts",
}
: {},
: {}),
"@extensions": path.resolve(__dirname, "../../extensions"),
},
},
test: {
reporters: "default",
Expand Down
13 changes: 13 additions & 0 deletions crates/extensions-runtime/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "extensions-runtime"
version = "0.1.0"
edition = "2021"

[dependencies]
deno_core = "0.338"

serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "sync"] }
tracing = { workspace = true }
25 changes: 25 additions & 0 deletions crates/extensions-runtime/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Extension not found: {0}")]
ExtensionNotFound(String),

#[error("Invalid manifest: {0}")]
InvalidManifest(String),

#[error("Runtime error: {0}")]
RuntimeError(String),

#[error("IO error: {0}")]
Io(#[from] std::io::Error),

#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),

#[error("Channel send error")]
ChannelSend,

#[error("Channel receive error")]
ChannelRecv,
}

pub type Result<T> = std::result::Result<T, Error>;
8 changes: 8 additions & 0 deletions crates/extensions-runtime/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mod error;
mod manifest;
mod ops;
mod runtime;

pub use error::*;
pub use manifest::*;
pub use runtime::*;
Loading
Loading