Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: keybindings tab ✨ #82

Merged
merged 6 commits into from
Aug 13, 2024
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
5 changes: 5 additions & 0 deletions .changeset/modern-deers-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tablex/core": patch
---

Adds keybindings tab + adds some extra datatypes in the backend
12 changes: 11 additions & 1 deletion apps/core/src-tauri/src/commands/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use specta::Type;
use tauri::AppHandle;
use tx_keybindings::get_keybindings_file_path;
use tx_keybindings::{get_keybindings_file_path, Keybinding};
use tx_lib::fs::{read_from_json, write_into_json};
use tx_settings::{get_settings_file_path, Settings};

Expand Down Expand Up @@ -50,3 +50,13 @@ pub fn write_into_settings_file(app: AppHandle, settings: Value) -> Result<(), S
write_into_json(&get_settings_file_path(&app)?, stored_settings)?;
Ok(())
}

#[tauri::command]
#[specta::specta]
pub fn write_into_keybindings_file(
app: AppHandle,
keybindings: Vec<Keybinding>,
) -> Result<(), String> {
write_into_json(&get_keybindings_file_path(&app)?, keybindings)?;
Ok(())
}
1 change: 1 addition & 0 deletions apps/core/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ fn main() {
open_in_external_editor,
load_settings_file,
write_into_settings_file,
write_into_keybindings_file,
// Table commands.
get_tables,
get_columns_props,
Expand Down
8 changes: 8 additions & 0 deletions apps/core/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ async writeIntoSettingsFile(settings: JsonValue) : Promise<Result<null, string>>
else return { status: "error", error: e as any };
}
},
async writeIntoKeybindingsFile(keybindings: Keybinding[]) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("write_into_keybindings_file", { keybindings }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getTables() : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_tables") };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { commands, type Keybinding } from "@/bindings"
import Kbd from "@/components/kbd"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table"
import { TabsContent } from "@/components/ui/tabs"
import { useKeybindings, type EditedBinding } from "@/keybindings/manager"
import { Edit2, FileJson2 } from "lucide-react"
import { useEffect, useState, type Dispatch, type SetStateAction } from "react"

const KeybindingsTab = () => {
const keybindings = useKeybindings()
const [editedKeybindings, setEditKeybindings] = useState<EditedBinding[]>([])
return (
<TabsContent value="keybindings">
<Table>
<TableHeader className="font-bold">
<TableRow>
<TableHead>Command</TableHead>
<TableHead>Shortcuts</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{keybindings.bindings.map((binding, idx) => (
<TableRow className="group border-zinc-700" key={idx}>
<TableCell>
<code>{binding.command}</code>
</TableCell>
<TableCell className="space-x-2">
{binding.shortcuts.map((b) => (
<Kbd>{b}</Kbd>
))}
</TableCell>
<TableCell>
<KeyRecorderDialog
binding={binding}
setEditedKeybindings={setEditKeybindings}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="absolute bottom-7 right-4 flex flex-row-reverse items-center gap-x-3">
<Button
size={"sm"}
onClick={async () => {
await commands.writeIntoKeybindingsFile(keybindings.bindings)
console.log(editedKeybindings)
keybindings.reRegister(editedKeybindings)
}}
>
Save
</Button>
<Button
variant={"outline"}
size={"icon"}
onClick={async () =>
await commands.openInExternalEditor("keybindings")
}
>
<FileJson2 className="h-4 w-4" />
</Button>
</div>
</TabsContent>
)
}

export default KeybindingsTab

type KeyRecorderProps = {
binding: Keybinding
setEditedKeybindings: Dispatch<SetStateAction<EditedBinding[]>>
}

const KeyRecorderDialog = ({
binding,
setEditedKeybindings
}: KeyRecorderProps) => {
const [key, setKey] = useState<string[]>([])
const recordKeys = (e: KeyboardEvent) => {
if (e.repeat) return
e.preventDefault()

let preparedKey: string

switch (e.key) {
// for the sake of hotkey-js to recognize them.
case "Control":
case "Shift":
case "Alt":
preparedKey = e.key.toLowerCase()
break
default:
preparedKey = e.key
}

setKey((old) => [...old, preparedKey])
}

useEffect(() => {
document.addEventListener("keydown", recordKeys)
return () => document.removeEventListener("keydown", recordKeys)
}, [key])

return (
<Dialog
onOpenChange={(open) => {
if (!open) {
document.removeEventListener("keydown", recordKeys)
}
}}
>
<DialogTrigger asChild className="invisible group-hover:visible">
<Edit2 className="h-4 w-4" role="button" />
</DialogTrigger>
<DialogContent className="space-y-3">
<DialogHeader>
<DialogTitle>
Press desired key combination and then press ENTER
</DialogTitle>
</DialogHeader>
<div className="flex h-full flex-col items-center gap-y-5">
<Input
className="bg-muted w-1/2"
value={key.join("+")}
readOnly
disabled
/>
<div className="flex items-center gap-x-4">
<Button variant={"secondary"} onClick={() => setKey([])}>
Reset
</Button>
<DialogClose asChild>
<Button
onClick={() => {
binding.shortcuts = [key.join("+")]
setEditedKeybindings((old) => [
...old,
{ command: binding.command, shortcuts: binding.shortcuts }
])
}}
>
Done
</Button>
</DialogClose>
</div>
</div>
</DialogContent>
</Dialog>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
import { TabsContent } from "@/components/ui/tabs"
import { useSettingsManager } from "@/settings/manager"
import { useSettings } from "@/settings/manager"
import { zodResolver } from "@hookform/resolvers/zod"
import { FileJson2 } from "lucide-react"
import type { PropsWithChildren } from "react"
Expand All @@ -33,7 +33,7 @@ const formSchema = z.object({
})

const SettingsTab = () => {
const settings = useSettingsManager()
const settings = useSettings()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: settings
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { usePreferencesState } from "@/state/dialogState"
import { lazy, Suspense } from "react"

import LoadingSpinner from "@/components/loading-spinner"
import hotkeys from "hotkeys-js"
import KeybindingsTab from "./components/keybindings-tab"
import SettingsTab from "./components/settings-tab"
const GeneralTab = lazy(() => import("./components/general-tab"))

Expand Down Expand Up @@ -36,7 +37,7 @@ const PreferencesDialog = () => {
<Suspense fallback={<LoadingSpinner />}>
<GeneralTab />
<SettingsTab />
<TabsContent value="keybindings">TODO</TabsContent>
<KeybindingsTab />
</Suspense>
</section>
</Tabs>
Expand Down
4 changes: 2 additions & 2 deletions apps/core/src/components/dialogs/sql-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
TableRow
} from "@/components/ui/table"
import { TooltipProvider } from "@/components/ui/tooltip"
import { useSettingsManager } from "@/settings/manager"
import { useSettings } from "@/settings/manager"

import { useSqlEditorState } from "@/state/dialogState"
import { Editor, type OnMount } from "@monaco-editor/react"
Expand All @@ -30,7 +30,7 @@ const SQLDialog = () => {
const [queryResult, setQueryResult] = useState<RawQueryResult>()
const [editorMounted, setEditorMounted] = useState(false)
const editorRef = useRef<MonakoEditor>()
const { sqlEditor: editorSettings } = useSettingsManager()
const { sqlEditor: editorSettings } = useSettings()

const handleEditorDidMount: OnMount = (editor) => {
setEditorMounted(true)
Expand Down
18 changes: 18 additions & 0 deletions apps/core/src/components/kbd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { cn } from "@tablex/lib/utils"
import { ComponentProps, PropsWithChildren } from "react"

const Kbd = (props: ComponentProps<"kbd"> & PropsWithChildren) => {
return (
<kbd
{...props}
className={cn(
props.className,
"bg-background rounded-md border-b border-white/55 px-2 py-1"
)}
>
{props.children}
</kbd>
)
}

export default Kbd
4 changes: 2 additions & 2 deletions apps/core/src/hooks/table.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getZodSchemaFromCols } from "@/commands/columns"
import { generateColumnsDefs } from "@/routes/dashboard/_layout/$tableName/-components/columns"
import { useSettingsManager } from "@/settings/manager"
import { useSettings } from "@/settings/manager"
import { useQuery } from "@tanstack/react-query"
import {
getCoreRowModel,
Expand Down Expand Up @@ -89,7 +89,7 @@ export const useSetupReactTable = <TData, TValue>({
* to be used in paginating the rows.
*/
const useSetupPagination = () => {
const settings = useSettingsManager()
const settings = useSettings()
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: settings.pageSize
Expand Down
42 changes: 32 additions & 10 deletions apps/core/src/keybindings/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ import { BaseDirectory, readTextFile } from "@tauri-apps/plugin-fs"
import hotkeys from "hotkeys-js"
import { createContext, useContext } from "react"

export type RegisteredBinding = {
command: KeybindingCommand
handler: () => void
}

export type EditedBinding = {
command: KeybindingCommand
shortcuts: string[]
}

/**
* Class responsible for loading the keybindings from the keybindings file,
* and register handlers for those keybindings on your desire.
*
* It can be used anywhere in the application through the {@link useKeybindingsManager} context hook.
* It can be used anywhere in the application through the {@link useKeybindings} context hook.
*/
export class KeybindingsManager {
private bindings: Keybinding[] = []
bindings: Keybinding[] = []
registeredKeybindings: RegisteredBinding[] = []

constructor() {
readTextFile(KEYBINDINGS_FILE_NAME, {
Expand All @@ -25,30 +36,41 @@ export class KeybindingsManager {
/**
* Register handlers keybindings' commands.
* Handlers will be called once the shortcut is triggered by the user.
* @param keybindings array of commands and their respective handlers.
* @param keybindings keybindings to be registered.
*/
registerKeybindings(
keybindings: {
command: KeybindingCommand
handler: () => void
}[]
) {
registerKeybindings(keybindings: RegisteredBinding[]) {
keybindings.forEach((keybinding) => {
const binding = this.bindings.find(
(binding) => binding.command == keybinding.command
)
if (binding) {
this.registeredKeybindings.push(keybinding)
hotkeys(binding.shortcuts.join(","), keybinding.handler)
}
})
}

/**
* ReRegister edited keybindings.
* @param keybindings edited keybindings
*/
reRegister(keybindings: EditedBinding[]) {
keybindings.forEach((keybinding) => {
const toBeReRegistered = this.registeredKeybindings.find(
(registered) => registered.command === keybinding.command
)
if (toBeReRegistered) {
hotkeys(keybinding.shortcuts.join(","), toBeReRegistered.handler)
}
})
}
}

export const KeybindingsManagerContext = createContext(new KeybindingsManager())

/**
* A react context hook to access the {@link KeybindingsManager} from anywhere in the application.
*/
export const useKeybindingsManager = () => {
export const useKeybindings = () => {
return useContext(KeybindingsManagerContext)
}
Loading
Loading