diff --git a/README.md b/README.md index 17b60b59..3d809a11 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ Unsupported files can still be converted to a supported format/codec from the `F ## [Import / export](import-export.md) -## [Command line interface (CLI)](cli.md) +## [Command line interface (CLI) / API](cli.md) ## [Known issues, limitations, troubleshooting, FAQ](issues.md) diff --git a/cli.md b/cli.md index 2395c684..8a80bec7 100644 --- a/cli.md +++ b/cli.md @@ -22,3 +22,32 @@ See [available settings](https://github.com/mifi/lossless-cut/blob/master/public ```bash LosslessCut --settings-json '{captureFormat:"jpeg", "keyframeCut":true}' ``` + +## Controlling a running instance (experimental) + +If you have the "Allow multiple instances" setting enabled, you can control a running instance of LosslessCut from the outside, using for example a command line. You do this by issuing messages to it through the `LosslessCut` command. Currently only keyboard actions are supported. *Note that this is considered experimental and the API may change at any time.* + +### Keyboard actions, `--keyboard-action` + +Simulate a keyboard press action. The available action names can be found in the "Keyboard shortcuts" dialog (Note: you don't have to bind them to any key). + +Example: + +```bash +# Export the currently opened file +LosslessCut --keyboard-action export +``` + +#### Batch example + +Note that there is no synchronization, and the action will exit immediately, regardless of how long the action takes. This means that you will have to sleep between multiple actions. + +```bash +for PROJECT in /path/to/folder/with/projects/*.llc + LosslessCut $PROJECT + sleep 5 # wait for the file to open + LosslessCut --keyboard-action export + sleep 10 # hopefully done by then + LosslessCut --keyboard-action quit +done +``` \ No newline at end of file diff --git a/public/electron.js b/public/electron.js index 6ab73cf3..3c5c9638 100644 --- a/public/electron.js +++ b/public/electron.js @@ -201,7 +201,11 @@ function initApp() { if (!Array.isArray(additionalData?.argv)) return; const argv2 = parseCliArgs(additionalData.argv); - if (argv2._) openFilesEventually(argv2._); + + logger.info('second-instance', argv2); + + if (argv2._ && argv2._.length > 0) openFilesEventually(argv2._); + else if (argv2.keyboardAction) mainWindow.webContents.send('apiKeyboardAction', argv2.keyboardAction); }); // Quit when all windows are closed. @@ -317,6 +321,10 @@ function focusWindow() { } } +function quitApp() { + electron.app.quit(); +} + const hasDisabledNetworking = () => !!disableNetworking; -module.exports = { focusWindow, isDev, hasDisabledNetworking }; +module.exports = { focusWindow, isDev, hasDisabledNetworking, quitApp }; diff --git a/src/App.jsx b/src/App.jsx index d585a06a..6e68c202 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -90,7 +90,7 @@ const filePathToUrl = window.require('file-url'); const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path'); const remote = window.require('@electron/remote'); -const { focusWindow, hasDisabledNetworking } = remote.require('./electron'); +const { focusWindow, hasDisabledNetworking, quitApp } = remote.require('./electron'); const calcShouldShowWaveform = (zoomedDuration) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8); @@ -1890,7 +1890,7 @@ const App = memo(() => { electron.clipboard.writeText(await formatTsv(selectedSegments)); }, [isFileOpened, selectedSegments]); - const onKeyPress = useCallback(({ action, keyup }) => { + const getKeyboardAction = useCallback(({ action, keyup }) => { function seekReset() { seekAccelerationRef.current = 1; } @@ -1996,10 +1996,15 @@ const App = memo(() => { decreaseVolume: () => setPlaybackVolume((val) => Math.max(0, val - 0.07)), copySegmentsToClipboard, reloadFile: () => setCacheBuster((v) => v + 1), + quit: () => quitApp(), }; + return mainActions[action]; + }, [addSegment, alignSegmentTimesToKeyframes, askSetStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, cleanupFilesDialog, clearSegments, closeBatch, combineOverlappingSegments, combineSelectedSegments, concatCurrentBatch, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, duplicateCurrentSegment, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, onExportPress, onLabelSegment, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleStreamsSelector, toggleStripAudio, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]); + + const onKeyPress = useCallback(({ action, keyup }) => { function tryMainActions() { - const fn = mainActions[action]; + const fn = getKeyboardAction({ action, keyup }); if (!fn) return { match: false }; const bubble = fn(); return { match: true, bubble }; @@ -2038,7 +2043,7 @@ const App = memo(() => { if (match) return bubble; return true; // bubble the event - }, [addSegment, alignSegmentTimesToKeyframes, askSetStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, cleanupFilesDialog, clearSegments, closeBatch, closeExportConfirm, combineOverlappingSegments, combineSelectedSegments, concatCurrentBatch, concatDialogVisible, convertFormatBatch, copySegmentsToClipboard, createFixedDurationSegments, createNumSegments, createRandomSegments, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, duplicateCurrentSegment, exportConfirmVisible, extractAllStreams, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, fillSegmentsGaps, goToTimecode, increaseRotation, invertAllSegments, invertSelectedSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyboardShortcutsVisible, onExportConfirm, onExportPress, onLabelSegment, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleKeyboardShortcuts, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleStreamsSelector, toggleStripAudio, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]); + }, [closeExportConfirm, concatDialogVisible, exportConfirmVisible, getKeyboardAction, keyboardShortcutsVisible, onExportConfirm, toggleKeyboardShortcuts]); useKeyboard({ keyBindings, onKeyPress }); @@ -2148,8 +2153,18 @@ const App = memo(() => { } } + function tryKeyboardAction(action) { + const fn = getKeyboardAction({ action }); + if (!fn) { + console.error('Action not found:', action); + return; + } + fn(); + } + const actions = { openFiles: (event, filePaths) => { userOpenFiles(filePaths.map(resolvePathIfNeeded)); }, + apiKeyboardAction: (event, action) => tryKeyboardAction(action), openFilesDialog, closeCurrentFile: () => { closeFileWithConfirm(); }, closeBatchFiles: () => { closeBatch(); }, @@ -2197,7 +2212,7 @@ const App = memo(() => { actionsWithCatch.forEach(([key, action]) => electron.ipcRenderer.on(key, action)); return () => actionsWithCatch.forEach(([key, action]) => electron.ipcRenderer.removeListener(key, action)); - }, [alignSegmentTimesToKeyframes, apparentCutSegments, askSetStartTimeOffset, checkFileOpened, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatCurrentBatch, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, customOutDir, cutSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, detectedFps, extractAllStreams, fileFormat, filePath, fillSegmentsGaps, getFrameCount, handleShowStreamsSelectorClick, invertAllSegments, loadCutSegments, loadMedia, openFilesDialog, openSendReportDialogWithState, reorderSegsByStartTime, selectedSegments, setWorking, shiftAllSegmentTimes, shuffleSegments, toggleKeyboardShortcuts, toggleLastCommands, toggleSettings, tryFixInvalidDuration, userHtml5ifyCurrentFile, userOpenFiles]); + }, [alignSegmentTimesToKeyframes, apparentCutSegments, askSetStartTimeOffset, checkFileOpened, clearSegments, closeBatch, closeFileWithConfirm, combineOverlappingSegments, combineSelectedSegments, concatCurrentBatch, createFixedDurationSegments, createNumSegments, createRandomSegments, createSegmentsFromKeyframes, customOutDir, cutSegments, detectBlackScenes, detectSceneChanges, detectSilentScenes, detectedFps, extractAllStreams, fileFormat, filePath, fillSegmentsGaps, getFrameCount, getKeyboardAction, handleShowStreamsSelectorClick, invertAllSegments, loadCutSegments, loadMedia, openFilesDialog, openSendReportDialogWithState, reorderSegsByStartTime, selectedSegments, setWorking, shiftAllSegmentTimes, shuffleSegments, toggleKeyboardShortcuts, toggleLastCommands, toggleSettings, tryFixInvalidDuration, userHtml5ifyCurrentFile, userOpenFiles]); const showAddStreamSourceDialog = useCallback(async () => { try { diff --git a/src/components/KeyboardShortcuts.jsx b/src/components/KeyboardShortcuts.jsx index 9abf3e60..16e833ad 100644 --- a/src/components/KeyboardShortcuts.jsx +++ b/src/components/KeyboardShortcuts.jsx @@ -504,6 +504,10 @@ const KeyboardShortcuts = memo(({ name: t('Close current screen'), category: otherCategory, }, + quit: { + name: t('Quit LosslessCut'), + category: otherCategory, + }, }, }; }, [currentCutSeg, t]); @@ -585,9 +589,11 @@ const KeyboardShortcuts = memo(({ return (
- {beforeContent} - - {actionName} +
+ {beforeContent} + {actionName} +
{action}
+