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
55 changes: 29 additions & 26 deletions src/components/main-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { OrganiseUI } from "./organise";
import { HeaderLayout } from "./header-layout";
import { PDFPreviewPane } from "./pdf-preview";
import { useFileListContext } from "../provider/fileListProvider";
import { KeyboardNavProvider } from "../provider/keyboardNavProvider";
import { Button } from "./ui/button";

interface MainUIProps {
Expand Down Expand Up @@ -50,33 +51,35 @@ export function MainUI(props: MainUIProps) {

return (
<HeaderLayout toolName={props.toolName} onBack={props.onBack}>
<box flexDirection="row">
<box flexGrow={3}>
<Dynamic component={toolComponents[props.selectedTool]} />
</box>
<Show when={props.selectedTool !== "organise" && props.selectedTool !== "decrypt"}>
<box flexGrow={2} width={isPreviewOpen() && fl.fileCount() > 0 ? "30%" : 6}>
<Show
when={isPreviewOpen() && fl.fileCount() > 0}
fallback={
<box alignItems="flex-start" paddingTop={2}>
<Button
disabled={!fl.selectedFile()}
color="yellow"
label="◀"
onClick={() => setIsPreviewOpen(true)}
/>
</box>
}
>
<PDFPreviewPane
onOpen={() => setIsPreviewOpen(true)}
onClose={() => setIsPreviewOpen(false)}
/>
</Show>
<KeyboardNavProvider>
<box flexDirection="row">
<box flexGrow={3}>
<Dynamic component={toolComponents[props.selectedTool]} />
</box>
</Show>
</box>
<Show when={props.selectedTool !== "organise" && props.selectedTool !== "decrypt"}>
<box flexGrow={2} width={isPreviewOpen() && fl.fileCount() > 0 ? "30%" : 6}>
<Show
when={isPreviewOpen() && fl.fileCount() > 0}
fallback={
<box alignItems="flex-start" paddingTop={2}>
<Button
disabled={!fl.selectedFile()}
color="yellow"
label="◀"
onClick={() => setIsPreviewOpen(true)}
/>
</box>
}
>
<PDFPreviewPane
onOpen={() => setIsPreviewOpen(true)}
onClose={() => setIsPreviewOpen(false)}
/>
</Show>
</box>
</Show>
</box>
</KeyboardNavProvider>
</HeaderLayout>
);
}
62 changes: 61 additions & 1 deletion src/components/pdf-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TextAttributes, type BoxRenderable, type CliRenderer } from "@opentui/core";
import { onResize, useRenderer } from "@opentui/solid";
import { useBindings } from "@opentui/keymap/solid";
import { Show, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js";
import {
clearPDFPreview,
Expand All @@ -12,6 +13,7 @@ import { useFileListContext } from "../provider/fileListProvider";
import { Button } from "./ui/button";
import { PreviewButton } from "./ui/preview-button";
import { PDFPreviewFrame } from "./ui/index";
import { useKeyboardNav } from "../hooks/useKeyboardNav";

type RendererCapabilities = CliRenderer["capabilities"];

Expand All @@ -21,6 +23,8 @@ const hasKittyGraphics = (capabilities: RendererCapabilities | null) =>
export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void }) {
const renderer = useRenderer();
const fl = useFileListContext();
const nav = useKeyboardNav();

const selectedFile = fl.selectedFile;
const initialKittySupport = hasKittyGraphics(renderer.capabilities ?? null);

Expand All @@ -46,12 +50,15 @@ export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void
let layoutRefreshTimer: ReturnType<typeof setTimeout> | null = null;

const totalPages = () => Math.max(pageCount() ?? 0, 1);

const canGoPrev = () => page() > 1;
const canGoNext = () => page() < totalPages();
const goToPrevPage = () => setPage((value) => Math.max(1, value - 1));
const goToNextPage = () => setPage((value) => Math.min(totalPages(), value + 1));

const showSupportProbe = () =>
Boolean(selectedFile()) && !supported() && !capabilityProbeExpired();
const showUnsupported = () => Boolean(selectedFile()) && !supported() && capabilityProbeExpired();
// const showLoading = () => Boolean(selectedFile()) && supported() && isLoading();
const showError = () =>
Boolean(selectedFile()) && supported() && !isLoading() && Boolean(error());

Expand Down Expand Up @@ -222,12 +229,63 @@ export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void
}
});

createEffect(() => {
nav.registerElement({
id: "prev-page",
type: "button",
persistent: true,
onEnter: () => setPage((value) => Math.max(1, value - 1)),
});

nav.registerElement({
id: "next-page",
type: "button",
persistent: true,
onEnter: () => setPage((value) => Math.min(totalPages(), value + 1)),
});
});

useBindings(() => ({
priority: 100,
bindings:
nav.isInputMode() || !selectedFile()
? []
: [
{
key: "left",
cmd: () => {
if (canGoPrev()) goToPrevPage();
},
},
{
key: "h",
cmd: () => {
if (canGoPrev()) goToPrevPage();
},
},
{
key: "right",
cmd: () => {
if (canGoNext()) goToNextPage();
},
},
{
key: "l",
cmd: () => {
if (canGoNext()) goToNextPage();
},
},
],
}));

onCleanup(() => {
renderer.off("capabilities", handleCapabilitiesChange);
cancelPendingPreviewRender();
clearCapabilityProbeTimer();
clearLayoutRefreshTimer();
clearActivePreview();
nav.unregisterElement("prev-page");
nav.unregisterElement("next-page");
});

return (
Expand Down Expand Up @@ -262,6 +320,7 @@ export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void
label="◀"
disabled={!canGoPrev()}
onClick={() => setPage((value) => Math.max(1, value - 1))}
focused={nav.isFocused("prev-page")}
/>
<box paddingBottom={1}>
<text
Expand All @@ -273,6 +332,7 @@ export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void
label="▶"
disabled={!canGoNext()}
onClick={() => setPage((value) => Math.min(totalPages(), value + 1))}
focused={nav.isFocused("next-page")}
/>
</box>
</Show>
Expand Down
15 changes: 10 additions & 5 deletions src/hooks/useKeyboardNav.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createSignal, createMemo } from "solid-js";
import { createSignal, createMemo, createContext, useContext } from "solid-js";
import { useBindings } from "@opentui/keymap/solid";
import type { FocusableElement } from "../model/models";

export type { FocusableElement };

export function useKeyboardNav() {
export function createKeyboardNav() {
const [focusIndex, setFocusIndex] = createSignal(0);
const [elements, setElements] = createSignal<FocusableElement[]>([]);
const [isInputMode, setIsInputMode] = createSignal(false);
Expand Down Expand Up @@ -62,19 +62,18 @@ export function useKeyboardNav() {
const getFocusedId = () => focusedElement()?.id ?? null;

const clearElements = () => {
setElements([]);
setElements((prev) => prev.filter((e) => e.persistent));
setFocusIndex(0);
};

useBindings(() => ({
priority: 100,
bindings: isInputMode()
? [
// Let escape bubble to the app-level double-esc handler in src/index.tsx
{
key: "escape",
cmd: () => setIsInputMode(false),
preventDefault: false,
preventDefault: false, // Let escape bubble to the app-level double-esc handler in src/index.tsx
},
{
key: "tab",
Expand Down Expand Up @@ -114,3 +113,9 @@ export function useKeyboardNav() {
setIsInputMode,
};
}

export const KeyboardNavContext = createContext<ReturnType<typeof createKeyboardNav>>();

export function useKeyboardNav() {
return useContext(KeyboardNavContext) ?? createKeyboardNav();
}
1 change: 1 addition & 0 deletions src/model/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface FocusableElement {
onEnter?: () => void;
canFocus?: () => boolean;
onMouseDown?: () => void;
persistent?: boolean; // keeps element registered across clearElements calls
}

export interface Status {
Expand Down
9 changes: 9 additions & 0 deletions src/provider/keyboardNavProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type ParentComponent } from "solid-js";
import { KeyboardNavContext, createKeyboardNav } from "../hooks/useKeyboardNav";

// Provides a single keyboard-nav instance so descendant panels (the active tool
// and the PDF preview) share one focus ring instead of competing rings.
export const KeyboardNavProvider: ParentComponent = (props) => {
const nav = createKeyboardNav();
return <KeyboardNavContext.Provider value={nav}>{props.children}</KeyboardNavContext.Provider>;
};
Loading