pcloud-ink

🖥️ The browser

PCloudBody and PCloudBrowser — the assembled interactive pCloud client, and the props a host uses to configure it.

PCloudBrowser (and its unwrapped inner component, PCloudBody) is the one export in this package that isn't presentation-only in the strict sense — it's a full interactive client, mounting the list components into tabs and owning its own navigation. What it does not own is the terminal: it never calls render() and it never calls process.exit. It reports quitting through the required onExit callback, so a host — a standalone CLI, or a pane in a larger cockpit — mounts it as a single component and keeps the terminal lifecycle, including render/unmount/waitUntilExit, entirely to itself.

import { render } from "ink"
import { PCloudBrowser } from "@kud/pcloud-ink"

const { unmount, waitUntilExit } = render(
  <PCloudBrowser onExit={() => unmount()} />,
  { alternateScreen: true },
)
await waitUntilExit()

PCloudBody vs PCloudBrowser

PCloudBrowser wraps PCloudBody in ink-picture's TerminalInfoProvider:

export const PCloudBrowser = (props: PCloudBodyProps) => (
  <TerminalInfoProvider>
    <PCloudBody {...props} />
  </TerminalInfoProvider>
)

That context is what image preview needs to size itself against the terminal. Mounting PCloudBody bare works, but previews silently stop rendering with no obvious reason why — so unless a host already provides its own TerminalInfoProvider further up the tree, reach for PCloudBrowser.

PCloudBodyProps

PropTypePurpose
onExit() => voidRequired. Called when the user quits. The host owns the terminal lifecycle, so acting on it — usually unmount() — is the host's job.
syncSyncProviderReads this machine's local sync pairs. Omit it and the Sync tab is absent entirely.
settingsSettingsProviderReads and writes pCloud Drive's local client settings. Omit it and the Settings tab is absent entirely.
apiPCloudAPIAn authenticated client. Omit and the stored credential is used — what the CLI wants when run standalone.
initialScreenModeThe screen to open on. Omit for Files.

sync and settings are providers rather than built-in behaviour because the data they return lives in a SQLite database on the host machine, not in the pCloud API — a rendering package has no business opening that file. A consumer with no local pCloud install has no database to read, and shouldn't be offered a tab that could only ever render empty. Passing neither yields a four-tab browser (Files, Rewind, Trash, Shares); passing both yields all six.

initialScreen deep-links straight to a named tab instead of requiring the user to navigate there. Two consumers lean on it: screenshot tooling that shoots one tab at a time, and developers iterating on a single panel who'd rather reload straight back into it than re-navigate on every restart. A request for a screen the current props can't offer (sync without a provider, say) falls back to Files rather than mounting a tab with nothing to show.

Mode and tabsFor

export type Mode = "files" | "trash" | "rewind" | "shares" | "sync" | "settings"

export const tabsFor: (has: {
  sync: boolean
  settings: boolean
}) => TabItem<Mode>[]

tabsFor is exported so a host can enumerate the screens it's actually able to offer, before ever mounting the browser — it's what backs --screen list and what validates a requested --screen <name> up front. It takes the same { sync, settings } shape the browser itself derives from its own props, so a host never has to hand-maintain a second copy of the tab list.

import { tabsFor, type Mode } from "@kud/pcloud-ink"

const SCREENS = tabsFor({ sync: true, settings: true }).map((tab) => tab.value)

A real embedding

@kud/pcloud-cli mounts PCloudBrowser with two host-owned providers — one reading pCloud Drive's sync database, one reading and writing its ignore rules — plus a mock mode that swaps every source (client, sync, settings) at once, so a screenshot never mixes invented files with real sync folders:

import { render } from "ink"
import { createMockAPI, mockSettings, mockSyncPairs } from "@kud/pcloud"
import { PCloudBrowser, type Mode } from "@kud/pcloud-ink"

export const startBrowse = async (mock = false, screen?: Mode) => {
  const { unmount, waitUntilExit } = render(
    mock ? (
      <PCloudBrowser
        onExit={() => unmount()}
        api={createMockAPI()}
        sync={mockSyncPairs}
        settings={{ read: mockSettings, write: () => {} }}
        initialScreen={screen}
      />
    ) : (
      <PCloudBrowser
        onExit={() => unmount()}
        sync={readSyncPairs}
        settings={{ read: readIgnoreRules, write: writeIgnoreRules }}
        initialScreen={screen}
      />
    ),
    { alternateScreen: true },
  )
  await waitUntilExit()
}

The settings write path is expected to throw when it can't persist — pCloud Drive rewrites its own settings from memory on quit, so a write made while the daemon is running would otherwise be undone silently, long after it looked to have worked.

On this page