From a5582919853f4d5b50a3097d3b85313c079efea5 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 11:04:07 +0200 Subject: [PATCH 1/6] feature: introduce play & pause media with the help of playerctl --- player-ctl/index.js | 6 ++++++ player-ctl/manifest.yml | 29 +++++++++++++++++++++++++++++ player-ctl/player-ctl-pause.js | 30 ++++++++++++++++++++++++++++++ player-ctl/player-ctl-play.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 player-ctl/index.js create mode 100644 player-ctl/manifest.yml create mode 100644 player-ctl/player-ctl-pause.js create mode 100644 player-ctl/player-ctl-play.js diff --git a/player-ctl/index.js b/player-ctl/index.js new file mode 100644 index 0000000..2567248 --- /dev/null +++ b/player-ctl/index.js @@ -0,0 +1,6 @@ +module.exports = { + commands: { + 'player-ctl-play': require('./player-ctl-play'), + 'player-ctl-pause': require('./player-ctl-pause'), + }, +}; diff --git a/player-ctl/manifest.yml b/player-ctl/manifest.yml new file mode 100644 index 0000000..669e4ff --- /dev/null +++ b/player-ctl/manifest.yml @@ -0,0 +1,29 @@ +name: player-ctl +label: Player Controls +version: 1.0.0 +author: Backslash + +commands: + - name: player-ctl-play + label: Play media + isImmediate: true + bgColor: "#2563EB" + color: "#FFFFFF" + icon: play + keywords: + - player + - media + - play + - resume + + - name: player-ctl-pause + label: Pause media + isImmediate: true + bgColor: "#2563EB" + color: "#FFFFFF" + icon: pause + keywords: + - player + - media + - pause + - stop diff --git a/player-ctl/player-ctl-pause.js b/player-ctl/player-ctl-pause.js new file mode 100644 index 0000000..31d498f --- /dev/null +++ b/player-ctl/player-ctl-pause.js @@ -0,0 +1,30 @@ +const execCommand = (exec, command) => + new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + const message = stderr?.trim() || error.message || "Unknown error"; + return reject(new Error(message)); + } + resolve(stdout); + }); + }); + +const ensurePlayerctl = async (exec) => { + try { + await execCommand(exec, "command -v playerctl"); + } catch { + throw new Error("playerctl is required to control media. Install it with your package manager."); + } +}; + +const run = async (_, { exec }) => { + await ensurePlayerctl(exec); + + try { + await execCommand(exec, "playerctl pause"); + } catch (error) { + throw new Error(`Failed to pause media playback: ${error.message}`); + } +}; + +module.exports = { run, actions: [] }; diff --git a/player-ctl/player-ctl-play.js b/player-ctl/player-ctl-play.js new file mode 100644 index 0000000..5b7cfb0 --- /dev/null +++ b/player-ctl/player-ctl-play.js @@ -0,0 +1,30 @@ +const execCommand = (exec, command) => + new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + const message = stderr?.trim() || error.message || "Unknown error"; + return reject(new Error(message)); + } + resolve(stdout); + }); + }); + +const ensurePlayerctl = async (exec) => { + try { + await execCommand(exec, "command -v playerctl"); + } catch { + throw new Error("playerctl is required to control media. Install it with your package manager."); + } +}; + +const run = async (_, { exec }) => { + await ensurePlayerctl(exec); + + try { + await execCommand(exec, "playerctl play"); + } catch (error) { + throw new Error(`Failed to start media playback: ${error.message}`); + } +}; + +module.exports = { run, actions: [] }; From ac4d8295625b3063ec562828c5f5763ae880c751 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 12:04:50 +0200 Subject: [PATCH 2/6] chore: update .gitignore with JetBrain files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1062418 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +*.iml From e6c31ee2448e8a10ac65eac11b79d53dda0ea92f Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 12:05:23 +0200 Subject: [PATCH 3/6] feat: Introduce commands on top of playerctl for play & pause --- player-ctl/player-ctl-pause.js | 15 +++++++++++---- player-ctl/player-ctl-play.js | 15 +++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/player-ctl/player-ctl-pause.js b/player-ctl/player-ctl-pause.js index 31d498f..ea0c4f8 100644 --- a/player-ctl/player-ctl-pause.js +++ b/player-ctl/player-ctl-pause.js @@ -9,16 +9,23 @@ const execCommand = (exec, command) => }); }); -const ensurePlayerctl = async (exec) => { +const ensurePlayerctl = async (exec, toast) => { try { await execCommand(exec, "command -v playerctl"); } catch { - throw new Error("playerctl is required to control media. Install it with your package manager."); + const message = + "playerctl is required to control media. Install it with your package manager."; + + if (toast?.error) { + toast.error("Missing dependency", { description: message }); + } + + throw new Error(message); } }; -const run = async (_, { exec }) => { - await ensurePlayerctl(exec); +const run = async (_, { exec, toast }) => { + await ensurePlayerctl(exec, toast); try { await execCommand(exec, "playerctl pause"); diff --git a/player-ctl/player-ctl-play.js b/player-ctl/player-ctl-play.js index 5b7cfb0..46cd4a2 100644 --- a/player-ctl/player-ctl-play.js +++ b/player-ctl/player-ctl-play.js @@ -9,16 +9,23 @@ const execCommand = (exec, command) => }); }); -const ensurePlayerctl = async (exec) => { +const ensurePlayerctl = async (exec, toast) => { try { await execCommand(exec, "command -v playerctl"); } catch { - throw new Error("playerctl is required to control media. Install it with your package manager."); + const message = + "playerctl is required to control media. Install it with your package manager."; + + if (toast?.error) { + toast.error("Missing dependency", { description: message }); + } + + throw new Error(message); } }; -const run = async (_, { exec }) => { - await ensurePlayerctl(exec); +const run = async (_, { exec, toast }) => { + await ensurePlayerctl(exec, toast); try { await execCommand(exec, "playerctl play"); From 6bed7da5d0c2b08b8b8855066dd798e95cc9f801 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 12:38:48 +0200 Subject: [PATCH 4/6] feat: add command for previous & next media --- player-ctl/index.js | 2 ++ player-ctl/manifest.yml | 24 ++++++++++++++++++++ player-ctl/player-ctl-next.js | 37 +++++++++++++++++++++++++++++++ player-ctl/player-ctl-previous.js | 37 +++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 player-ctl/player-ctl-next.js create mode 100644 player-ctl/player-ctl-previous.js diff --git a/player-ctl/index.js b/player-ctl/index.js index 2567248..b55b67c 100644 --- a/player-ctl/index.js +++ b/player-ctl/index.js @@ -2,5 +2,7 @@ module.exports = { commands: { 'player-ctl-play': require('./player-ctl-play'), 'player-ctl-pause': require('./player-ctl-pause'), + 'player-ctl-next': require('./player-ctl-next'), + 'player-ctl-previous': require('./player-ctl-previous'), }, }; diff --git a/player-ctl/manifest.yml b/player-ctl/manifest.yml index 669e4ff..484e487 100644 --- a/player-ctl/manifest.yml +++ b/player-ctl/manifest.yml @@ -27,3 +27,27 @@ commands: - media - pause - stop + + - name: player-ctl-next + label: Next media item + isImmediate: true + bgColor: "#2563EB" + color: "#FFFFFF" + icon: fast-forward + keywords: + - player + - media + - next + - skip + + - name: player-ctl-previous + label: Previous media item + isImmediate: true + bgColor: "#2563EB" + color: "#FFFFFF" + icon: rewind + keywords: + - player + - media + - previous + - back diff --git a/player-ctl/player-ctl-next.js b/player-ctl/player-ctl-next.js new file mode 100644 index 0000000..579ec05 --- /dev/null +++ b/player-ctl/player-ctl-next.js @@ -0,0 +1,37 @@ +const execCommand = (exec, command) => + new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + const message = stderr?.trim() || error.message || "Unknown error"; + return reject(new Error(message)); + } + resolve(stdout); + }); + }); + +const ensurePlayerctl = async (exec, toast) => { + try { + await execCommand(exec, "command -v playerctl"); + } catch { + const message = + "playerctl is required to control media. Install it with your package manager."; + + if (toast?.error) { + toast.error("Missing dependency", { description: message }); + } + + throw new Error(message); + } +}; + +const run = async (_, { exec, toast }) => { + await ensurePlayerctl(exec, toast); + + try { + await execCommand(exec, "playerctl next"); + } catch (error) { + throw new Error(`Failed to skip to the next item: ${error.message}`); + } +}; + +module.exports = { run, actions: [] }; diff --git a/player-ctl/player-ctl-previous.js b/player-ctl/player-ctl-previous.js new file mode 100644 index 0000000..40acb74 --- /dev/null +++ b/player-ctl/player-ctl-previous.js @@ -0,0 +1,37 @@ +const execCommand = (exec, command) => + new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + const message = stderr?.trim() || error.message || "Unknown error"; + return reject(new Error(message)); + } + resolve(stdout); + }); + }); + +const ensurePlayerctl = async (exec, toast) => { + try { + await execCommand(exec, "command -v playerctl"); + } catch { + const message = + "playerctl is required to control media. Install it with your package manager."; + + if (toast?.error) { + toast.error("Missing dependency", { description: message }); + } + + throw new Error(message); + } +}; + +const run = async (_, { exec, toast }) => { + await ensurePlayerctl(exec, toast); + + try { + await execCommand(exec, "playerctl previous"); + } catch (error) { + throw new Error(`Failed to go to the previous item: ${error.message}`); + } +}; + +module.exports = { run, actions: [] }; From aa18c5af11d484f97171dc372d958b7e73a10f14 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 12:56:34 +0200 Subject: [PATCH 5/6] feat(player-ctl): clear search after successful commands --- player-ctl/player-ctl-next.js | 3 ++- player-ctl/player-ctl-pause.js | 3 ++- player-ctl/player-ctl-play.js | 3 ++- player-ctl/player-ctl-previous.js | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/player-ctl/player-ctl-next.js b/player-ctl/player-ctl-next.js index 579ec05..9286381 100644 --- a/player-ctl/player-ctl-next.js +++ b/player-ctl/player-ctl-next.js @@ -24,11 +24,12 @@ const ensurePlayerctl = async (exec, toast) => { } }; -const run = async (_, { exec, toast }) => { +const run = async (_, { exec, toast, search }) => { await ensurePlayerctl(exec, toast); try { await execCommand(exec, "playerctl next"); + search?.clear?.(); } catch (error) { throw new Error(`Failed to skip to the next item: ${error.message}`); } diff --git a/player-ctl/player-ctl-pause.js b/player-ctl/player-ctl-pause.js index ea0c4f8..c3f6493 100644 --- a/player-ctl/player-ctl-pause.js +++ b/player-ctl/player-ctl-pause.js @@ -24,11 +24,12 @@ const ensurePlayerctl = async (exec, toast) => { } }; -const run = async (_, { exec, toast }) => { +const run = async (_, { exec, toast, search }) => { await ensurePlayerctl(exec, toast); try { await execCommand(exec, "playerctl pause"); + search?.clear?.(); } catch (error) { throw new Error(`Failed to pause media playback: ${error.message}`); } diff --git a/player-ctl/player-ctl-play.js b/player-ctl/player-ctl-play.js index 46cd4a2..e1d3394 100644 --- a/player-ctl/player-ctl-play.js +++ b/player-ctl/player-ctl-play.js @@ -24,11 +24,12 @@ const ensurePlayerctl = async (exec, toast) => { } }; -const run = async (_, { exec, toast }) => { +const run = async (_, { exec, toast, search }) => { await ensurePlayerctl(exec, toast); try { await execCommand(exec, "playerctl play"); + search?.clear?.(); } catch (error) { throw new Error(`Failed to start media playback: ${error.message}`); } diff --git a/player-ctl/player-ctl-previous.js b/player-ctl/player-ctl-previous.js index 40acb74..66e4045 100644 --- a/player-ctl/player-ctl-previous.js +++ b/player-ctl/player-ctl-previous.js @@ -24,11 +24,12 @@ const ensurePlayerctl = async (exec, toast) => { } }; -const run = async (_, { exec, toast }) => { +const run = async (_, { exec, toast, search }) => { await ensurePlayerctl(exec, toast); try { await execCommand(exec, "playerctl previous"); + search?.clear?.(); } catch (error) { throw new Error(`Failed to go to the previous item: ${error.message}`); } From 6306caa500cdb52edaca45c7e4302b7798dce54b Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 16:13:31 +0200 Subject: [PATCH 6/6] feat: shared utils for error handling of 'no player' --- player-ctl/player-ctl-next.js | 38 ++++--------------- player-ctl/player-ctl-pause.js | 38 ++++--------------- player-ctl/player-ctl-play.js | 38 ++++--------------- player-ctl/player-ctl-previous.js | 38 ++++--------------- player-ctl/utils.js | 62 +++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 120 deletions(-) create mode 100644 player-ctl/utils.js diff --git a/player-ctl/player-ctl-next.js b/player-ctl/player-ctl-next.js index 9286381..037c3f3 100644 --- a/player-ctl/player-ctl-next.js +++ b/player-ctl/player-ctl-next.js @@ -1,37 +1,15 @@ -const execCommand = (exec, command) => - new Promise((resolve, reject) => { - exec(command, (error, stdout, stderr) => { - if (error) { - const message = stderr?.trim() || error.message || "Unknown error"; - return reject(new Error(message)); - } - resolve(stdout); - }); - }); - -const ensurePlayerctl = async (exec, toast) => { - try { - await execCommand(exec, "command -v playerctl"); - } catch { - const message = - "playerctl is required to control media. Install it with your package manager."; - - if (toast?.error) { - toast.error("Missing dependency", { description: message }); - } +const { runPlayerctlCommand } = require("./utils"); - throw new Error(message); - } -}; +const run = async (_, context) => { + const { search } = context; -const run = async (_, { exec, toast, search }) => { - await ensurePlayerctl(exec, toast); + const didRun = await runPlayerctlCommand(context, { + command: "playerctl next", + errorPrefix: "Failed to skip to the next item", + }); - try { - await execCommand(exec, "playerctl next"); + if (didRun) { search?.clear?.(); - } catch (error) { - throw new Error(`Failed to skip to the next item: ${error.message}`); } }; diff --git a/player-ctl/player-ctl-pause.js b/player-ctl/player-ctl-pause.js index c3f6493..1cfe046 100644 --- a/player-ctl/player-ctl-pause.js +++ b/player-ctl/player-ctl-pause.js @@ -1,37 +1,15 @@ -const execCommand = (exec, command) => - new Promise((resolve, reject) => { - exec(command, (error, stdout, stderr) => { - if (error) { - const message = stderr?.trim() || error.message || "Unknown error"; - return reject(new Error(message)); - } - resolve(stdout); - }); - }); - -const ensurePlayerctl = async (exec, toast) => { - try { - await execCommand(exec, "command -v playerctl"); - } catch { - const message = - "playerctl is required to control media. Install it with your package manager."; - - if (toast?.error) { - toast.error("Missing dependency", { description: message }); - } +const { runPlayerctlCommand } = require("./utils"); - throw new Error(message); - } -}; +const run = async (_, context) => { + const { search } = context; -const run = async (_, { exec, toast, search }) => { - await ensurePlayerctl(exec, toast); + const didRun = await runPlayerctlCommand(context, { + command: "playerctl pause", + errorPrefix: "Failed to pause media playback", + }); - try { - await execCommand(exec, "playerctl pause"); + if (didRun) { search?.clear?.(); - } catch (error) { - throw new Error(`Failed to pause media playback: ${error.message}`); } }; diff --git a/player-ctl/player-ctl-play.js b/player-ctl/player-ctl-play.js index e1d3394..a384496 100644 --- a/player-ctl/player-ctl-play.js +++ b/player-ctl/player-ctl-play.js @@ -1,37 +1,15 @@ -const execCommand = (exec, command) => - new Promise((resolve, reject) => { - exec(command, (error, stdout, stderr) => { - if (error) { - const message = stderr?.trim() || error.message || "Unknown error"; - return reject(new Error(message)); - } - resolve(stdout); - }); - }); - -const ensurePlayerctl = async (exec, toast) => { - try { - await execCommand(exec, "command -v playerctl"); - } catch { - const message = - "playerctl is required to control media. Install it with your package manager."; - - if (toast?.error) { - toast.error("Missing dependency", { description: message }); - } +const { runPlayerctlCommand } = require("./utils"); - throw new Error(message); - } -}; +const run = async (_, context) => { + const { search } = context; -const run = async (_, { exec, toast, search }) => { - await ensurePlayerctl(exec, toast); + const didRun = await runPlayerctlCommand(context, { + command: "playerctl play", + errorPrefix: "Failed to start media playback", + }); - try { - await execCommand(exec, "playerctl play"); + if (didRun) { search?.clear?.(); - } catch (error) { - throw new Error(`Failed to start media playback: ${error.message}`); } }; diff --git a/player-ctl/player-ctl-previous.js b/player-ctl/player-ctl-previous.js index 66e4045..5d05a31 100644 --- a/player-ctl/player-ctl-previous.js +++ b/player-ctl/player-ctl-previous.js @@ -1,37 +1,15 @@ -const execCommand = (exec, command) => - new Promise((resolve, reject) => { - exec(command, (error, stdout, stderr) => { - if (error) { - const message = stderr?.trim() || error.message || "Unknown error"; - return reject(new Error(message)); - } - resolve(stdout); - }); - }); - -const ensurePlayerctl = async (exec, toast) => { - try { - await execCommand(exec, "command -v playerctl"); - } catch { - const message = - "playerctl is required to control media. Install it with your package manager."; - - if (toast?.error) { - toast.error("Missing dependency", { description: message }); - } +const { runPlayerctlCommand } = require("./utils"); - throw new Error(message); - } -}; +const run = async (_, context) => { + const { search } = context; -const run = async (_, { exec, toast, search }) => { - await ensurePlayerctl(exec, toast); + const didRun = await runPlayerctlCommand(context, { + command: "playerctl previous", + errorPrefix: "Failed to go to the previous item", + }); - try { - await execCommand(exec, "playerctl previous"); + if (didRun) { search?.clear?.(); - } catch (error) { - throw new Error(`Failed to go to the previous item: ${error.message}`); } }; diff --git a/player-ctl/utils.js b/player-ctl/utils.js new file mode 100644 index 0000000..a82d4c1 --- /dev/null +++ b/player-ctl/utils.js @@ -0,0 +1,62 @@ +const execCommand = (exec, command) => + new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + const message = stderr?.trim() || error.message || "Unknown error"; + return reject(new Error(message)); + } + resolve(stdout); + }); + }); + +const ensurePlayerctl = async ({ exec, toast }) => { + try { + await execCommand(exec, "command -v playerctl"); + } catch { + const message = + "playerctl is required to control media. Install it with your package manager."; + + if (toast?.error) { + toast.error("Missing dependency", { description: message }); + } + + throw new Error(message); + } +}; + +const handleNoPlayersFound = (toast) => { + if (toast?.error) { + toast.error("No media players running", { + description: "Start playback in your media player and try again.", + }); + } +}; + +const runPlayerctlCommand = async ( + context, + { command, errorPrefix } +) => { + const { exec, toast } = context; + + await ensurePlayerctl(context); + + try { + await execCommand(exec, command); + return true; + } catch (error) { + const message = error?.message || "Unknown error"; + + if (message.toLowerCase().includes("no players found")) { + handleNoPlayersFound(toast); + return false; + } + + throw new Error(`${errorPrefix}: ${message}`); + } +}; + +module.exports = { + execCommand, + ensurePlayerctl, + runPlayerctlCommand, +};