v3
can’t innovate my ass Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com>
This commit is contained in:
+1289
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
||||
import type { ToolbarCommandId } from './types'
|
||||
|
||||
const COPY_DESCRIPTION =
|
||||
'Copy files from a source to a destination without deleting destination files.'
|
||||
const MOVE_DESCRIPTION =
|
||||
'Move files from a source to a destination and delete them from the source.'
|
||||
const SYNC_DESCRIPTION =
|
||||
'Sync source to destination, updating existing files and removing stale ones.'
|
||||
const MOUNT_DESCRIPTION = 'Mount a remote to the local filesystem with VFS options.'
|
||||
const DOWNLOAD_DESCRIPTION = 'Download a URL directly into a remote or local path.'
|
||||
const SERVE_DESCRIPTION = 'Serve a remote over HTTP, WebDAV, SFTP, FTP and Restic.'
|
||||
const BISYNC_DESCRIPTION = 'Bi-directional sync keeping source and destination in parity.'
|
||||
const DELETE_DESCRIPTION = 'Delete files or folders from a remote or local path.'
|
||||
const PURGE_DESCRIPTION = 'Purge an entire path from a remote, deleting everything.'
|
||||
const CLEANUP_DESCRIPTION = 'Cleanup a remote by removing trashed and partial files.'
|
||||
const BROWSE_DESCRIPTION = 'Browse files and folders in a remote.'
|
||||
const SETTINGS_DESCRIPTION = 'Open the Settings screen.'
|
||||
const GITHUB_DESCRIPTION = 'Open an issue or check out the GitHub repository.'
|
||||
const TRANSFERS_DESCRIPTION = 'Open the Transfers screen.'
|
||||
const SCHEDULES_DESCRIPTION = 'Open the Schedules screen.'
|
||||
const TEMPLATES_DESCRIPTION = 'Open the Templates screen.'
|
||||
const REMOTE_CREATE_DESCRIPTION = 'Create a new remote.'
|
||||
const REMOTE_EDIT_DESCRIPTION = 'Edit a remote.'
|
||||
const REMOTE_AUTO_MOUNT_DESCRIPTION = 'Configure auto mount options for a remote.'
|
||||
const REMOTE_LIST_DESCRIPTION = 'Show all configured remotes.'
|
||||
const QUIT_DESCRIPTION = 'Quit the application.'
|
||||
|
||||
export const COMMAND_CONFIG = {
|
||||
copy: { route: '/copy', windowLabel: 'Copy' },
|
||||
move: { route: '/move', windowLabel: 'Move' },
|
||||
sync: { route: '/sync', windowLabel: 'Sync' },
|
||||
mount: { route: '/mount', windowLabel: 'Mount' },
|
||||
download: { route: '/download', windowLabel: 'Download' },
|
||||
serve: { route: '/serve', windowLabel: 'Serve' },
|
||||
bisync: { route: '/bisync', windowLabel: 'Bisync' },
|
||||
delete: { route: '/delete', windowLabel: 'Delete' },
|
||||
purge: { route: '/purge', windowLabel: 'Purge' },
|
||||
cleanup: {},
|
||||
browse: {},
|
||||
settings: { route: '/settings', windowLabel: 'Settings' },
|
||||
github: { route: '/github', windowLabel: 'GitHub' },
|
||||
transfers: { route: '/transfers', windowLabel: 'Transfers' },
|
||||
schedules: { route: '/schedules', windowLabel: 'Schedules' },
|
||||
templates: { route: '/templates', windowLabel: 'Templates' },
|
||||
remoteCreate: {},
|
||||
remoteEdit: {},
|
||||
remoteAutoMount: {},
|
||||
remoteList: {},
|
||||
quit: {},
|
||||
} as const
|
||||
|
||||
export const COMMAND_DESCRIPTIONS: Record<ToolbarCommandId, string> = {
|
||||
copy: COPY_DESCRIPTION,
|
||||
move: MOVE_DESCRIPTION,
|
||||
sync: SYNC_DESCRIPTION,
|
||||
mount: MOUNT_DESCRIPTION,
|
||||
download: DOWNLOAD_DESCRIPTION,
|
||||
serve: SERVE_DESCRIPTION,
|
||||
bisync: BISYNC_DESCRIPTION,
|
||||
delete: DELETE_DESCRIPTION,
|
||||
purge: PURGE_DESCRIPTION,
|
||||
cleanup: CLEANUP_DESCRIPTION,
|
||||
browse: BROWSE_DESCRIPTION,
|
||||
settings: SETTINGS_DESCRIPTION,
|
||||
github: GITHUB_DESCRIPTION,
|
||||
transfers: TRANSFERS_DESCRIPTION,
|
||||
schedules: SCHEDULES_DESCRIPTION,
|
||||
templates: TEMPLATES_DESCRIPTION,
|
||||
remoteCreate: REMOTE_CREATE_DESCRIPTION,
|
||||
remoteEdit: REMOTE_EDIT_DESCRIPTION,
|
||||
remoteAutoMount: REMOTE_AUTO_MOUNT_DESCRIPTION,
|
||||
remoteList: REMOTE_LIST_DESCRIPTION,
|
||||
quit: QUIT_DESCRIPTION,
|
||||
}
|
||||
|
||||
export const COMMAND_KEYWORDS: Record<ToolbarCommandId, string[]> = {
|
||||
copy: ['copy', 'cp', 'transfer'],
|
||||
move: ['move', 'mv'],
|
||||
sync: ['sync', 'synchronise', 'synchronize'],
|
||||
mount: ['mount'],
|
||||
download: ['download', 'url', 'copyurl', 'copyto'],
|
||||
serve: ['serve', 'http', 'webdav', 'sftp', 'ftp', 'restic'],
|
||||
bisync: ['bisync'],
|
||||
delete: ['delete', 'remove', 'rm'],
|
||||
purge: ['purge', 'empty'],
|
||||
cleanup: ['cleanup', 'clean'],
|
||||
browse: ['browse', 'explore', 'open', 'view', 'files'],
|
||||
settings: ['settings', 'config', 'preferences'],
|
||||
github: ['github', 'issue', 'bug', 'feature'],
|
||||
transfers: ['transfer', 'job', 'task'],
|
||||
schedules: ['schedule', 'cron', 'task'],
|
||||
templates: ['template', 'example'],
|
||||
remoteCreate: ['new', 'remote', 'create'],
|
||||
remoteEdit: ['edit', 'remote', 'update', 'change'],
|
||||
remoteAutoMount: ['mount', 'remote', 'update', 'change'],
|
||||
remoteList: ['remote', 'list', 'show'],
|
||||
quit: ['quit', 'exit', 'close'],
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { getToolbarAction, getToolbarActions } from './actions'
|
||||
import type {
|
||||
ToolbarActionArgs,
|
||||
ToolbarActionContext,
|
||||
ToolbarActionDefinition,
|
||||
ToolbarActionPath,
|
||||
ToolbarActionResult,
|
||||
ToolbarCommandId,
|
||||
} from './types'
|
||||
|
||||
export interface ResolvedToolbarResult {
|
||||
id: string
|
||||
actionId: ToolbarCommandId
|
||||
label: string
|
||||
description?: string
|
||||
args: ToolbarActionArgs
|
||||
score: number
|
||||
resolve: () => ToolbarActionDefinition
|
||||
}
|
||||
|
||||
export function runToolbarEngine(
|
||||
query: string,
|
||||
remotes: string[],
|
||||
remoteTypes?: Record<string, string>
|
||||
) {
|
||||
const actions = getToolbarActions()
|
||||
|
||||
const trimmed = query.trim()
|
||||
|
||||
const parsedPaths = extractPaths(trimmed, remotes, remoteTypes)
|
||||
|
||||
const cleanedQuery = trimmed.replace(parsedPaths.map((path) => path.full).join(' '), '').trim()
|
||||
|
||||
const results = trimmed
|
||||
? collectActionResults(actions, {
|
||||
query: cleanedQuery,
|
||||
fullQuery: trimmed,
|
||||
paths: parsedPaths,
|
||||
})
|
||||
: buildDefaultResults(actions, remotes)
|
||||
|
||||
const finalResults = results.length > 0 ? results : buildDefaultResults(actions, remotes)
|
||||
|
||||
return {
|
||||
results: finalResults.sort((a, b) => b.score - a.score),
|
||||
}
|
||||
}
|
||||
|
||||
function collectActionResults(
|
||||
actions: ToolbarActionDefinition[],
|
||||
context: ToolbarActionContext
|
||||
): ResolvedToolbarResult[] {
|
||||
const collected: ResolvedToolbarResult[] = []
|
||||
|
||||
for (const action of actions) {
|
||||
const results = action.getResults(context)
|
||||
for (const result of results) {
|
||||
collected.push(mapResult(action, result))
|
||||
}
|
||||
}
|
||||
|
||||
return dedupeResults(collected)
|
||||
}
|
||||
|
||||
function buildDefaultResults(
|
||||
actions: ToolbarActionDefinition[],
|
||||
remotes: string[]
|
||||
): ResolvedToolbarResult[] {
|
||||
return actions
|
||||
.map((action) =>
|
||||
action.getDefaultResult ? mapResult(action, action.getDefaultResult({ remotes })) : null
|
||||
)
|
||||
.filter((result): result is ResolvedToolbarResult => result !== null)
|
||||
}
|
||||
|
||||
function mapResult(
|
||||
action: ToolbarActionDefinition,
|
||||
result: ToolbarActionResult
|
||||
): ResolvedToolbarResult {
|
||||
return {
|
||||
id: serializeResult(action.id, result.args),
|
||||
actionId: action.id,
|
||||
label: result.label ?? action.label,
|
||||
description: result.description ?? action.description,
|
||||
args: result.args ?? {},
|
||||
score: result.score ?? 0,
|
||||
resolve: () => getToolbarAction(action.id),
|
||||
}
|
||||
}
|
||||
|
||||
function dedupeResults(results: ResolvedToolbarResult[]): ResolvedToolbarResult[] {
|
||||
const bestById = new Map<string, ResolvedToolbarResult>()
|
||||
|
||||
for (const result of results) {
|
||||
const key = `${result.actionId}:${JSON.stringify(result.args)}`
|
||||
const existing = bestById.get(key)
|
||||
if (!existing || result.score > existing.score) {
|
||||
bestById.set(key, result)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(bestById.values())
|
||||
}
|
||||
|
||||
function serializeResult(actionId: ToolbarCommandId, args: ToolbarActionArgs) {
|
||||
return `${actionId}:${JSON.stringify(args)}`
|
||||
}
|
||||
|
||||
const WHITESPACE_SPLIT = /\s+/
|
||||
const TOKEN_TRIM_REGEX = /^[\"'`]+|[\"'`.,;!?]+$/g
|
||||
const REMOTE_PATH_REGEX = /^([^:\s]+):(.*)$/
|
||||
const WINDOWS_DRIVE_REGEX = /^[a-zA-Z]:[\\/]/
|
||||
const ALPHA_REGEX = /[a-zA-Z]/
|
||||
const WINDOWS_PREFIX_REGEX = /^([a-zA-Z]:)(.*)$/
|
||||
|
||||
function extractPaths(
|
||||
input: string,
|
||||
remotes: string[],
|
||||
remoteTypes?: Record<string, string>
|
||||
): ToolbarActionPath[] {
|
||||
const matches = input.split(WHITESPACE_SPLIT).filter(Boolean)
|
||||
const seen = new Set<string>()
|
||||
const results: ToolbarActionPath[] = []
|
||||
const separator = sep()
|
||||
|
||||
console.log('remotes', remotes)
|
||||
|
||||
for (const raw of matches) {
|
||||
const cleaned = stripToken(raw)
|
||||
if (!cleaned) continue
|
||||
let remoteName: string | undefined
|
||||
let remoteType: string | undefined
|
||||
let isLocal: boolean = false
|
||||
|
||||
// eagerly match remotes
|
||||
if (remotes.includes(cleaned)) {
|
||||
remoteName = cleaned
|
||||
remoteType = remoteTypes?.[cleaned]
|
||||
} else if (isRemotePath(cleaned)) {
|
||||
// remote path match (e.g., "rct:/path/to/file")
|
||||
const match = REMOTE_PATH_REGEX.exec(cleaned)
|
||||
if (match && remotes.includes(match[1])) {
|
||||
remoteName = match[1]
|
||||
remoteType = remoteTypes?.[match[1]]
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if (isLocalPath(cleaned)) {
|
||||
isLocal = true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if (!seen.has(cleaned)) {
|
||||
seen.add(cleaned)
|
||||
results.push({
|
||||
full: cleaned,
|
||||
readable: createReadablePath(cleaned, isLocal, separator),
|
||||
isLocal,
|
||||
remoteName,
|
||||
remoteType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function createReadablePath(full: string, isLocal: boolean, separator: string): string {
|
||||
return isLocal ? createLocalReadable(full, separator) : createRemoteReadable(full)
|
||||
}
|
||||
|
||||
function createRemoteReadable(full: string): string {
|
||||
const match = REMOTE_PATH_REGEX.exec(full)
|
||||
if (!match) {
|
||||
return full
|
||||
}
|
||||
const remote = match[1]
|
||||
let remainder = match[2] ?? ''
|
||||
if (!remainder || remainder === '/') {
|
||||
return `${remote}:/`
|
||||
}
|
||||
if (remainder.startsWith('/')) {
|
||||
remainder = remainder.slice(1)
|
||||
}
|
||||
const segments = remainder.split('/').filter(Boolean)
|
||||
if (segments.length === 0) {
|
||||
return `${remote}:/`
|
||||
}
|
||||
const last = segments[segments.length - 1]
|
||||
if (segments.length === 1) {
|
||||
return `${remote}:/${last}`
|
||||
}
|
||||
if (segments.length === 2) {
|
||||
return `${remote}:/${segments[0]}/${last}`
|
||||
}
|
||||
const secondToLast = segments[segments.length - 2]
|
||||
return `${remote}:/${segments[0]}/.../${secondToLast}/${last}`
|
||||
}
|
||||
|
||||
function createLocalReadable(full: string, separator: string): string {
|
||||
const normalized = full.replace(/[/\\]+/g, separator)
|
||||
|
||||
const windowsMatch = WINDOWS_PREFIX_REGEX.exec(normalized)
|
||||
if (windowsMatch) {
|
||||
const drive = windowsMatch[1]
|
||||
const remainder = windowsMatch[2] ?? ''
|
||||
const segments = remainder.split(separator).filter(Boolean)
|
||||
if (segments.length === 0) {
|
||||
return `${drive}${separator}`
|
||||
}
|
||||
const last = segments.pop() ?? ''
|
||||
if (segments.length === 0) {
|
||||
return `${drive}${separator}${last}`
|
||||
}
|
||||
if (segments.length === 1) {
|
||||
return `${drive}${separator}${segments[0]}${separator}${last}`
|
||||
}
|
||||
const secondToLast = segments.pop() ?? ''
|
||||
const first = segments.shift() ?? ''
|
||||
return `${drive}${separator}${first}${separator}...${separator}${secondToLast}${separator}${last}`
|
||||
}
|
||||
|
||||
const isAbsolute =
|
||||
normalized.startsWith(separator) ||
|
||||
normalized.startsWith('/') ||
|
||||
normalized.startsWith('\\')
|
||||
const segments = normalized.split(separator).filter(Boolean)
|
||||
if (segments.length === 0) {
|
||||
return isAbsolute ? separator : normalized
|
||||
}
|
||||
const last = segments.pop() ?? ''
|
||||
if (!isAbsolute && segments.length === 0) {
|
||||
return last || normalized
|
||||
}
|
||||
const first = segments.shift()
|
||||
if (!first) {
|
||||
return isAbsolute ? `${separator}${last}` : last
|
||||
}
|
||||
if (segments.length === 0) {
|
||||
return isAbsolute
|
||||
? `${separator}${first}${separator}${last}`
|
||||
: `${first}${separator}${last}`
|
||||
}
|
||||
if (segments.length === 1) {
|
||||
return isAbsolute
|
||||
? `${separator}${first}${separator}${segments[0]}${separator}${last}`
|
||||
: `${first}${separator}${segments[0]}${separator}${last}`
|
||||
}
|
||||
const secondToLast = segments.pop() ?? ''
|
||||
return isAbsolute
|
||||
? `${separator}${first}${separator}...${separator}${secondToLast}${separator}${last}`
|
||||
: `${first}${separator}...${separator}${secondToLast}${separator}${last}`
|
||||
}
|
||||
|
||||
function stripToken(token: string): string {
|
||||
return token.replace(TOKEN_TRIM_REGEX, '')
|
||||
}
|
||||
|
||||
function isRemotePath(token: string): boolean {
|
||||
if (!token.includes(':')) return false
|
||||
if (token.includes('://')) return false
|
||||
|
||||
const match = REMOTE_PATH_REGEX.exec(token)
|
||||
if (!match) return false
|
||||
|
||||
if (isWindowsDrive(match[1], match[2])) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function isLocalPath(token: string): boolean {
|
||||
if (token.startsWith('/')) return true
|
||||
if (token.startsWith('~/')) return true
|
||||
if (token.startsWith('./') || token.startsWith('../')) return true
|
||||
if (WINDOWS_DRIVE_REGEX.test(token)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function isWindowsDrive(prefix: string, remainder: string): boolean {
|
||||
return (
|
||||
prefix.length === 1 &&
|
||||
ALPHA_REGEX.test(prefix) &&
|
||||
(remainder.startsWith('\\') || remainder.startsWith('/'))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export type ToolbarCommandId =
|
||||
| 'copy'
|
||||
| 'move'
|
||||
| 'sync'
|
||||
| 'mount'
|
||||
| 'download'
|
||||
| 'serve'
|
||||
| 'bisync'
|
||||
| 'delete'
|
||||
| 'purge'
|
||||
| 'cleanup'
|
||||
| 'browse'
|
||||
| 'settings'
|
||||
| 'github'
|
||||
| 'transfers'
|
||||
| 'schedules'
|
||||
| 'templates'
|
||||
| 'remoteCreate'
|
||||
| 'remoteEdit'
|
||||
| 'remoteAutoMount'
|
||||
| 'quit'
|
||||
| 'remoteList'
|
||||
|
||||
export type ToolbarActionArgs = Record<string, any>
|
||||
|
||||
export interface ToolbarActionResult {
|
||||
label: string
|
||||
description?: string
|
||||
args: ToolbarActionArgs
|
||||
score?: number
|
||||
}
|
||||
|
||||
export interface ToolbarActionOnPressContext {
|
||||
openWindow: (options: { name: string; url: string }) => Promise<unknown>
|
||||
}
|
||||
|
||||
export interface ToolbarActionPath {
|
||||
full: string
|
||||
readable: string
|
||||
isLocal: boolean
|
||||
remoteName?: string
|
||||
remoteType?: string
|
||||
}
|
||||
|
||||
export interface ToolbarActionContext {
|
||||
query: string
|
||||
fullQuery: string
|
||||
paths: ToolbarActionPath[]
|
||||
}
|
||||
|
||||
export interface ToolbarActionDefaultContext {
|
||||
remotes: string[]
|
||||
}
|
||||
|
||||
export interface ToolbarActionDefinition {
|
||||
id: ToolbarCommandId
|
||||
label: string
|
||||
description?: string
|
||||
keywords: string[]
|
||||
getResults: (context: ToolbarActionContext) => ToolbarActionResult[]
|
||||
getDefaultResult?: (context: ToolbarActionDefaultContext) => ToolbarActionResult
|
||||
onPress: (args: ToolbarActionArgs, context: ToolbarActionOnPressContext) => Promise<void> | void
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { fetchMountList, fetchServeList } from '../lib/rclone/api'
|
||||
|
||||
export function formatServeInfo(serve: Awaited<ReturnType<typeof fetchServeList>>[number]): string {
|
||||
const parts = [`ID: ${serve.id}`, `Address: ${serve.addr}`]
|
||||
|
||||
if (serve.params?.opt?.password) {
|
||||
parts.push(`Password: ${serve.params.opt.password}`)
|
||||
}
|
||||
|
||||
if (serve.params?.type) {
|
||||
parts.push(`Type: ${serve.params.type.toUpperCase()}`)
|
||||
}
|
||||
if (serve.params?.fs) {
|
||||
parts.push(`Source: ${serve.params.fs}`)
|
||||
}
|
||||
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
export function formatServeLabel(
|
||||
serve: Awaited<ReturnType<typeof fetchServeList>>[number]
|
||||
): string {
|
||||
const type = serve.params?.type?.toUpperCase() ?? 'SERVE'
|
||||
const fs = serve.params?.fs ?? 'unknown'
|
||||
return `${type} · ${fs} · ${serve.addr}`
|
||||
}
|
||||
|
||||
export function formatMountLabel(
|
||||
mount: Awaited<ReturnType<typeof fetchMountList>>[number]
|
||||
): string {
|
||||
return `MOUNT · ${mount.Fs} · ${mount.MountPoint}`
|
||||
}
|
||||
Reference in New Issue
Block a user