diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 8da9261..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "gnim-utils"] - path = src/utils - url = https://github.com/retrozinndev/gnim-utils.git diff --git a/package.json b/package.json index 5c310ed..44f95a4 100644 --- a/package.json +++ b/package.json @@ -14,5 +14,8 @@ }, "devDependencies": { "ags": "link:../../../../usr/share/ags/js" + }, + "dependencies": { + "gnim-utils": "github:retrozinndev/gnim-utils" } } diff --git a/src/app.ts b/src/app.ts index ddcc993..e232b6c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,5 @@ -import "ags/overrides"; +// thanks Aylur!! +import "../node_modules/ags/lib/overrides"; import "./config"; import { PluginApps, @@ -9,7 +10,6 @@ import { PluginWebSearch, PluginKill } from "./runner/plugins"; -import { Wireplumber } from "./modules/volume"; import { handleArguments } from "./modules/arg-handler"; import { Runner } from "./runner/Runner"; import { Windows } from "./windows"; @@ -32,6 +32,7 @@ import GObject, { register } from "ags/gobject"; import GLib from "gi://GLib?version=2.0"; import Gio from "gi://Gio?version=2.0"; import Adw from "gi://Adw?version=1"; +import AstalWp from "gi://AstalWp"; const runnerPlugins: Array = [ @@ -284,11 +285,24 @@ you should use the socket in the XDG_RUNTIME_DIR/colorshell.sock for a faster re console.log("Adding runner plugins"); runnerPlugins.forEach(plugin => Runner.addPlugin(plugin)); - this.#connections.set(Wireplumber.getDefault(), - Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => - !Windows.getDefault().isOpen("control-center") && - triggerOSD(OSDModes.sink) - ) + createSubscription( + secureBaseBinding( + createBinding(AstalWp.get_default(), "defaultSpeaker"), + "volume", + null + ), + () => !Windows.getDefault().isOpen("control-center") && + triggerOSD(OSDModes.sink) + ); + + createSubscription( + secureBaseBinding( + createBinding(AstalWp.get_default(), "defaultSpeaker"), + "mute", + null + ), + () => !Windows.getDefault().isOpen("control-center") && + triggerOSD(OSDModes.sink) ); createSubscription( diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..8e7bb39 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,160 @@ +import { Scope } from "ags"; +import { createScopedConnection, encoder } from "../modules/utils"; + +import windows from "./modules/windows"; +import volume from "./modules/volume"; +import devel from "./modules/devel"; +import media from "./modules/media"; +import Gio from "gi://Gio?version=2.0"; +import GLib from "gi://GLib?version=2.0"; + + +export namespace Cli { + let rootScope: Scope; + let service: Gio.SocketService; + let initialized: boolean = false; + const modules: Array = [ + // main module, no need for prefix + { + help: "manage colorshell windows and do more cool stuff.", + commands: [ + ...windows, + // others + { + name: "runner", + onCalled: (_, data) => { + return { + content: `Opening runner${data ? ` with "${data}"` : ""}...`, + type: "out" + }; + } + } + ], + arguments: [ + { + name: "version", + alias: "v", + help: "print the current colorshell version", + onCalled: () => `colorshell by retrozinndev, version ${COLORSHELL_VERSION + }${DEVEL ? "(devel)" : ""}` + } + ] + }, + volume, + media + ]; + + export type Output = { + type: "err"|"out"; + content: string|Uint8Array; + } | string; + + /** argument passed to the command / module. + * output of onCalled is passed to */ + export type Argument = { + /** kebab-cased name for the argument(without the `--` prefix) + * @example help (turns into `--help` internally)*/ + name: string; + /** alias for the name (without the `-` prefix). + * @example help -> h */ + alias?: string; + /** whether the argument needs a value attribute. + * @example --file ~/a_nice_home_file.txt */ + hasValue?: boolean; + /** runtime-set value for the argument(if enabled) */ + value?: string; + /** help message for the argument */ + help?: string; + onCalled: (value?: string) => void; + }; + + export type Command = { + /** the command name to be called. + * @example `colorshell ${prefix?} ${command.name}`*/ + name: string; + help?: string; + /** data passed to the command. (only works when arguments are disabled) */ + data?: string; + arguments?: Array; + onCalled: (args: Array, data?: string) => Output; + }; + + export type Module = { + /** command to come after the cli call. + * @example `colorshell ${prefix?} ${command}`*/ + prefix?: string; + commands?: Array; + arguments?: Array; + help?: string; + /** called everytime the prefix is used, even when using module commands */ + onPrefixCalled?: () => void; + }; + + /** initialize the cli */ + export function init(scope: Scope, socketService: Gio.SocketService): void { + if(initialized) return; + + initialized = true; + rootScope = scope; + service = socketService; + DEVEL && modules.push(devel); + + scope.run(() => { + createScopedConnection( + service, "incoming", (conn) => { + try { + return handleIncoming(conn); + } catch(_) {} + + return false; + } + ); + }); + } + + /** handle incoming socket calls */ + function handleIncoming(conn: Gio.SocketConnection): void { + const inputStream = Gio.DataInputStream.new(conn.inputStream); + + inputStream.read_upto_async('\x00', -1, GLib.PRIORITY_DEFAULT, null, (_, res) => { + const [args, len] = inputStream.read_upto_finish(res); + inputStream.close(null); + conn.inputStream.close(null); + + if(len < 1) { + console.error(`Colorshell: No args provided via socket call`); + return; + } + + try { + const [success, parsedArgs] = GLib.shell_parse_argv(`colorshell ${args}`); + parsedArgs?.splice(0, 1); // remove the unnecessary `colorshell` part + + if(success) { + handleArgs(parsedArgs!); + + conn.outputStream.flush(null); + conn.close(null); + return; + } + + conn.outputStream.write_bytes( + encoder.encode("Error: Unexpected syntax error occurred"), + null + ); + + conn.outputStream.flush(null); + conn.close(null); + } catch(_e) { + const e = _e as Error; + console.error(`Colorshell: An error occurred while writing to socket output. Stderr:\n${ + e.message}\n${e.stack}`); + } + }); + } + + /** translate app arguments to modules/commands */ + function handleArgs(args: Array): void { + let mod: Module; + } +} diff --git a/src/cli/modules/devel.ts b/src/cli/modules/devel.ts new file mode 100644 index 0000000..f2d5e4c --- /dev/null +++ b/src/cli/modules/devel.ts @@ -0,0 +1,16 @@ +import { Gtk } from "ags/gtk4"; +import { Cli } from ".."; + + +export default { + prefix: "dev", + help: "development tools to help debugging colorshell", + commands: [{ + name: "inspector", + help: "open the gtk's visual inspector", + onCalled: () => { + Gtk.Window.set_interactive_debugging(true); + return "Opening GTK Inspector..." + } + }] +} satisfies Cli.Module; diff --git a/src/cli/modules/media.ts b/src/cli/modules/media.ts new file mode 100644 index 0000000..c05bbef --- /dev/null +++ b/src/cli/modules/media.ts @@ -0,0 +1,46 @@ +import { Cli } from ".."; + + +export default { + prefix: "media", + help: "manage colorshell's active player", + commands: [ + { + name: "play", + help: "resume/start active player's media", + onCalled: () => "TODO" + }, { + name: "pause", + help: "pause the active player", + onCalled: () => "TODO" + }, { + name: "play-pause", + help: "toggle pause/resume the active player", + onCalled: () => "TODO" + }, { + name: "stop", + help: "stop the active player (if compatible)", + onCalled: () => "TODO" + }, { + name: "previous", + help: "go back to previous media in the active player", + onCalled: () => "TODO" + }, { + name: "next", + help: "jump to the next media in active player", + onCalled: () => "TODO" + }, { + name: "bus-name", + help: "retrieve the active player's mpris bus name", + onCalled: () => "TODO" + }, { + name: "list", + help: "list available players implementing mpris", + onCalled: () => "TODO" + }, { + name: "select", + help: "resume/start active player's media", + onCalled: (_, busName) => "TODO" + } + ] +} satisfies Cli.Module; diff --git a/src/cli/modules/volume.ts b/src/cli/modules/volume.ts new file mode 100644 index 0000000..73773a1 --- /dev/null +++ b/src/cli/modules/volume.ts @@ -0,0 +1,27 @@ +import { Cli } from ".."; + + +export default { + prefix: "volume", + help: "manage audio device volume/sensitivity. available devices are sink(speaker) and source(microphone).\ +example usage: `colorshell volume increase sink 5%`", + commands: [ + { + name: "increase", + help: "increase volume/sensitivity of a sink/source", + onCalled: () => "TODO" + }, { + name: "decrease", + help: "decrease volume/sensitivity of a sink/source", + onCalled: () => "TODO" + }, { + name: "set", + help: "set the volume/sensitivity of a sink/source", + onCalled: () => "TODO" + }, { + name: "mute", + help: "toggle-mute a sink/source's audio", + onCalled: () => "TODO" + } + ] +} satisfies Cli.Module; diff --git a/src/cli/modules/windows.ts b/src/cli/modules/windows.ts new file mode 100644 index 0000000..e6cf02f --- /dev/null +++ b/src/cli/modules/windows.ts @@ -0,0 +1,46 @@ +import { Cli } from ".."; + + +export default [ + { + name: "open", + onCalled: (_, data) => { + return { + type: "out", + content: "TODO" + } + } + }, { + name: "toggle", + onCalled: (_, data) => { + return { + type: "out", + content: "TODO" + } + } + }, { + name: "close", + onCalled: (_, data) => { + return { + type: "out", + content: "TODO" + } + } + }, { + name: "windows", + onCalled: () => { + return { + type: "out", + content: "TODO" + } + } + }, { + name: "reopen", + onCalled: () => { + return { + type: "out", + content: "TODO" + } + } + } +] satisfies Array; diff --git a/src/modules/auth.ts b/src/modules/auth.ts index ec1cb2d..3332a70 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -2,29 +2,29 @@ import { exec, execAsync } from "ags/process"; import { register } from "ags/gobject"; import { AuthPopup } from "../widget/AuthPopup"; -import AstalAuth from "gi://AstalAuth"; -import Polkit from "gi://Polkit"; -import PolkitAgent from "gi://PolkitAgent"; +import Polkit from "gi://Polkit?version=1.0"; +import PolkitAgent from "gi://PolkitAgent?version=1.0"; import Gio from "gi://Gio?version=2.0"; import GLib from "gi://GLib?version=2.0"; +import AstalAuth from "gi://AstalAuth?version=0.1"; @register({ GTypeName: "AuthAgent" }) export class Auth extends PolkitAgent.Listener { private static instance: Auth; - #subject: Polkit.Subject; - #pam: AstalAuth.Pam; #handle: any; + #user: Polkit.UnixUser; + #pam: AstalAuth.Pam; constructor() { super(); - this.#subject = Polkit.UnixSession.new(""); - this.#pam = new AstalAuth.Pam(); + this.#user = Polkit.UnixUser.new(Number.parseInt(exec("id -u"))) as Polkit.UnixUser; + this.#pam = new AstalAuth.Pam; - this.#handle = this.register( - PolkitAgent.RegisterFlags.RUN_IN_THREAD, - this.#subject, - "/io/github/retrozinndev/colorshell/PolicyKit/AuthAgent", + this.register( + PolkitAgent.RegisterFlags.RUN_IN_THREAD, + Polkit.UnixSession.new(this.#user.get_uid().toString()), + "/io/github/retrozinndev/colorshell/AuthAgent", null ); } @@ -57,12 +57,8 @@ export class Auth extends PolkitAgent.Listener { } - - public static initAgent(): Auth { - if(!this.instance) - this.instance = new Auth(); - - return this.instance; + initiate_authentication_finish(res: Gio.AsyncResult): boolean { + } // TODO: support fingerprint/facial auth diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 2a65287..c3aef5e 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -17,7 +17,7 @@ export { createScopedConnection, createSecureBinding as secureBinding, createSecureAccessorBinding as secureBaseBinding, -} from "../utils"; +} from "gnim-utils"; export const decoder = new TextDecoder("utf-8"), diff --git a/src/modules/volume.ts b/src/modules/volume.ts index 590dba9..c247e6c 100644 --- a/src/modules/volume.ts +++ b/src/modules/volume.ts @@ -1,11 +1,8 @@ -import GObject, { register } from "ags/gobject"; import AstalWp from "gi://AstalWp"; -export { Wireplumber }; -@register({ GTypeName: "Wireplumber" }) -class Wireplumber extends GObject.Object { - private static astalWireplumber: (AstalWp.Wp|null) = AstalWp.get_default(); +export class Wireplumber { + private static astalWireplumber: AstalWp.Wp|null = AstalWp.get_default(); private static inst: Wireplumber; private defaultSink: AstalWp.Endpoint = Wireplumber.astalWireplumber!.get_default_speaker()!; @@ -15,8 +12,6 @@ class Wireplumber extends GObject.Object { private maxSourceVolume: number = 100; constructor() { - super(); - if(!Wireplumber.astalWireplumber) throw new Error("Audio features will not work correctly! Please install wireplumber first", { cause: "Wireplumber library not found" diff --git a/src/utils b/src/utils deleted file mode 160000 index bedadf7..0000000 --- a/src/utils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bedadf7a2283310416cf186483fad852061cc3ee