feat(pulse): Convert from GJS to Rust
This commit is contained in:
parent
d6e93877ad
commit
d16a9a6494
8 changed files with 180 additions and 218 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -110,6 +110,16 @@ dependencies = [
|
||||||
"futures-micro",
|
"futures-micro",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-pidfd"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12177058299bb8e3507695941b6d0d7dc0e4e6515b8bc1bf4609d9e32ef51799"
|
||||||
|
dependencies = [
|
||||||
|
"async-io",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-process"
|
name = "async-process"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -901,6 +911,7 @@ dependencies = [
|
||||||
name = "pop-launcher-plugins"
|
name = "pop-launcher-plugins"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-pidfd",
|
||||||
"fork",
|
"fork",
|
||||||
"freedesktop-desktop-entry",
|
"freedesktop-desktop-entry",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -84,7 +84,7 @@ install:
|
||||||
install -Dm0755 plugins/src/recent/recent.js $(PLUGIN_DIR)/recent
|
install -Dm0755 plugins/src/recent/recent.js $(PLUGIN_DIR)/recent
|
||||||
|
|
||||||
# Pulse plugin
|
# Pulse plugin
|
||||||
install -Dm0755 plugins/src/pulse/pulse.js $(PLUGIN_DIR)/pulse
|
ln -sf $(BIN) $(PLUGIN_DIR)/pulse/pulse
|
||||||
|
|
||||||
# Terminal plugin
|
# Terminal plugin
|
||||||
install -Dm0755 plugins/src/terminal/terminal.js $(PLUGIN_DIR)/terminal
|
install -Dm0755 plugins/src/terminal/terminal.js $(PLUGIN_DIR)/terminal
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ fn main() {
|
||||||
"files" => block_on(plugins::files::main()),
|
"files" => block_on(plugins::files::main()),
|
||||||
"pop-launcher" => block_on(service::main()),
|
"pop-launcher" => block_on(service::main()),
|
||||||
"pop-shell" => block_on(plugins::pop_shell::main()),
|
"pop-shell" => block_on(plugins::pop_shell::main()),
|
||||||
|
"pulse" => block_on(plugins::pulse::main()),
|
||||||
"scripts" => block_on(plugins::scripts::main()),
|
"scripts" => block_on(plugins::scripts::main()),
|
||||||
"web" => block_on(plugins::web::main()),
|
"web" => block_on(plugins::web::main()),
|
||||||
unknown => {
|
unknown => {
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,4 @@ zbus = "1"
|
||||||
zvariant = "=2.6" # Restrict for 1.47
|
zvariant = "=2.6" # Restrict for 1.47
|
||||||
human-sort = "0.2.2"
|
human-sort = "0.2.2"
|
||||||
human_format = "1.0.3"
|
human_format = "1.0.3"
|
||||||
|
async-pidfd = "0.1.4"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod desktop_entries;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
pub mod find;
|
pub mod find;
|
||||||
pub mod pop_shell;
|
pub mod pop_shell;
|
||||||
|
pub mod pulse;
|
||||||
pub mod scripts;
|
pub mod scripts;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
|
||||||
|
|
|
||||||
164
plugins/src/pulse/mod.rs
Normal file
164
plugins/src/pulse/mod.rs
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
use async_pidfd::AsyncPidFd;
|
||||||
|
use futures_lite::prelude::*;
|
||||||
|
use pop_launcher::*;
|
||||||
|
use smol::Unblock;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
struct Selection {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
selections: Vec<Selection>,
|
||||||
|
out: Unblock<io::Stdout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for App {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
out: async_stdout(),
|
||||||
|
selections: vec![
|
||||||
|
Selection {
|
||||||
|
id: 0,
|
||||||
|
name: "Toggle Mute".into(),
|
||||||
|
description: "Silence and unsilence the default audio sink".into(),
|
||||||
|
},
|
||||||
|
Selection {
|
||||||
|
id: 1,
|
||||||
|
name: "Volume Up".into(),
|
||||||
|
description: "Raise volume 5%".into(),
|
||||||
|
},
|
||||||
|
Selection {
|
||||||
|
id: 2,
|
||||||
|
name: "Volume Down".into(),
|
||||||
|
description: "Lower volume 5%".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn main() {
|
||||||
|
let mut requests = json_input_stream(async_stdin());
|
||||||
|
|
||||||
|
let mut app = App::default();
|
||||||
|
|
||||||
|
while let Some(result) = requests.next().await {
|
||||||
|
match result {
|
||||||
|
Ok(request) => match request {
|
||||||
|
Request::Activate(id) => app.activate(id).await,
|
||||||
|
Request::Search(query) => app.search(query).await,
|
||||||
|
Request::Exit => break,
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Err(why) => {
|
||||||
|
tracing::error!("malformed JSON input: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
async fn activate(&mut self, id: u32) {
|
||||||
|
let (cmd, arg1, arg2) = match id {
|
||||||
|
0 => ("pactl", "set-sink-mute", "toggle"),
|
||||||
|
1 => ("pactl", "set-sink-volume", "+5%"),
|
||||||
|
2 => ("pactl", "set-sink-volume", "-5%"),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
|
let mut sinks = pactl_sinks();
|
||||||
|
|
||||||
|
use postage::prelude::Stream;
|
||||||
|
while let Some(id) = sinks.recv().await {
|
||||||
|
handles.push(smol::spawn(async move {
|
||||||
|
let args = &[arg1, id.as_str(), arg2];
|
||||||
|
let _ = command_spawn(cmd, args).await;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for handle in handles {
|
||||||
|
let _ = handle.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search(&mut self, query: String) {
|
||||||
|
if !query.is_empty() {
|
||||||
|
for selection in filter(&self.selections, &query.to_ascii_lowercase()) {
|
||||||
|
crate::send(
|
||||||
|
&mut self.out,
|
||||||
|
PluginResponse::Append(PluginSearchResult {
|
||||||
|
id: selection.id,
|
||||||
|
name: selection.name.clone(),
|
||||||
|
description: selection.description.clone(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::send(&mut self.out, PluginResponse::Finished).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter<'a>(
|
||||||
|
selections: &'a [Selection],
|
||||||
|
query: &'a str,
|
||||||
|
) -> impl Iterator<Item = &'a Selection> + 'a {
|
||||||
|
selections.iter().filter_map(move |selection| {
|
||||||
|
if selection.name.to_ascii_lowercase().contains(query)
|
||||||
|
|| selection.description.to_ascii_lowercase().contains(query)
|
||||||
|
{
|
||||||
|
Some(selection)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn command_spawn(cmd: &str, args: &[&str]) -> io::Result<()> {
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
let child = Command::new(cmd)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.args(args)
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
AsyncPidFd::from_pid(child.id() as i32)?.wait().await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pactl_sinks() -> postage::mpsc::Receiver<String> {
|
||||||
|
let (mut tx, rx) = postage::mpsc::channel(4);
|
||||||
|
|
||||||
|
smol::spawn(async move {
|
||||||
|
let child = smol::process::Command::new("pactl")
|
||||||
|
.env("LANG", "C")
|
||||||
|
.args(&["list", "sinks"])
|
||||||
|
.stdout(smol::process::Stdio::piped())
|
||||||
|
.spawn();
|
||||||
|
|
||||||
|
if let Ok(mut child) = child {
|
||||||
|
if let Some(stdout) = child.stdout.take() {
|
||||||
|
let mut lines = futures_lite::io::BufReader::new(stdout).lines();
|
||||||
|
while let Some(Ok(line)) = lines.next().await {
|
||||||
|
if let Some(stripped) = line.strip_prefix("Sink #") {
|
||||||
|
use postage::prelude::Sink;
|
||||||
|
tx.send(stripped.trim().to_owned()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
(
|
(
|
||||||
name: "PulseAudio Volume Control",
|
name: "PulseAudio Volume Control",
|
||||||
description: "Control PulseAudio devices and volume",
|
description: "Control PulseAudio devices and volume",
|
||||||
bin: (path: "pulse.js"),
|
bin: (path: "pulse"),
|
||||||
icon: Name("multimedia-volume-control")
|
icon: Name("multimedia-volume-control")
|
||||||
)
|
)
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
#!/usr/bin/gjs
|
|
||||||
|
|
||||||
const { GLib, Gio } = imports.gi;
|
|
||||||
|
|
||||||
const STDIN = new Gio.DataInputStream({ base_stream: new Gio.UnixInputStream({ fd: 0 }) })
|
|
||||||
const STDOUT = new Gio.DataOutputStream({ base_stream: new Gio.UnixOutputStream({ fd: 1 }) })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} Sink
|
|
||||||
* @property {number} id
|
|
||||||
* @property {string} description
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {null | Array<Sink>}
|
|
||||||
*/
|
|
||||||
function pactl_sinks() {
|
|
||||||
try {
|
|
||||||
const resp = async_process(["pactl", "list", "sinks"])
|
|
||||||
if (!resp) return null
|
|
||||||
|
|
||||||
const { proc, stdout } = resp
|
|
||||||
|
|
||||||
let sinks = new Array()
|
|
||||||
let sink = {}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const [bytes] = stdout.read_line(null)
|
|
||||||
if (bytes === null) break
|
|
||||||
|
|
||||||
const line = imports.byteArray.toString(bytes)
|
|
||||||
if (line.startsWith("Sink")) {
|
|
||||||
sink.id = line.substr(6)
|
|
||||||
} else if (line.includes("Description:")) {
|
|
||||||
sink.description = line.split(' ').slice(1).join(' ')
|
|
||||||
sinks.push({ ...sink })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sinks
|
|
||||||
} catch (e) {
|
|
||||||
log(`error: ${e}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class App {
|
|
||||||
constructor() {
|
|
||||||
this.last_query = ""
|
|
||||||
this.shell_only = false
|
|
||||||
|
|
||||||
this.default_selections = [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
name: "Toggle Mute",
|
|
||||||
description: "Silence and unsilence the default audio sink",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Volume Up",
|
|
||||||
description: "Raise volume 5%"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Volume Down",
|
|
||||||
description: "Lower volume 5%"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
query(input) {
|
|
||||||
const selections = filter_selections(this.default_selections, input.toLowerCase());
|
|
||||||
for (const selection of selections) {
|
|
||||||
this.send({ "Append": selection})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.send("Finished")
|
|
||||||
}
|
|
||||||
|
|
||||||
submit(id) {
|
|
||||||
let cmd = null
|
|
||||||
|
|
||||||
let sinks = pactl_sinks()
|
|
||||||
|
|
||||||
switch (id) {
|
|
||||||
case 0:
|
|
||||||
cmd = ["pactl set-sink-mute", "toggle"]
|
|
||||||
break
|
|
||||||
case 1:
|
|
||||||
cmd = ["pactl set-sink-volume", "+5%"]
|
|
||||||
break
|
|
||||||
case 2:
|
|
||||||
cmd = ["pactl set-sink-volume", "-5%"]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd) {
|
|
||||||
try {
|
|
||||||
for (const { id } of sinks) {
|
|
||||||
GLib.spawn_command_line_async(`${cmd[0]} ${id} ${cmd[1]}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
log(`session command '${cmd}' failed: ${e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
send(object) {
|
|
||||||
STDOUT.write_bytes(new GLib.Bytes(JSON.stringify(object) + "\n"), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
/** @type {null | ByteArray} */
|
|
||||||
let input_array
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
let input_str
|
|
||||||
|
|
||||||
/** @type {null | LauncherRequest} */
|
|
||||||
let event
|
|
||||||
|
|
||||||
let app = new App()
|
|
||||||
|
|
||||||
mainloop:
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
[input_array,] = STDIN.read_line(null)
|
|
||||||
} catch (e) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
input_str = imports.byteArray.toString(input_array)
|
|
||||||
if ((event = parse_event(input_str)) !== null) {
|
|
||||||
if ("Exit" === event) {
|
|
||||||
break mainloop
|
|
||||||
} else if ("Search" in event) {
|
|
||||||
app.query(event.Search)
|
|
||||||
} else if ("Activate" in event) {
|
|
||||||
app.submit(event.Activate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses an IPC event received from STDIN
|
|
||||||
* @param {string} input
|
|
||||||
* @returns {null | LauncherRequest}
|
|
||||||
*/
|
|
||||||
function parse_event(input) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(input)
|
|
||||||
} catch (e) {
|
|
||||||
log(`Input not valid JSON`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Array<string>} argv
|
|
||||||
* @returns {null | Process}
|
|
||||||
*/
|
|
||||||
function async_process(argv) {
|
|
||||||
const { DataInputStream, SubprocessFlags, SubprocessLauncher } = Gio
|
|
||||||
|
|
||||||
try {
|
|
||||||
const launcher = new SubprocessLauncher({
|
|
||||||
flags: SubprocessFlags.STDIN_PIPE
|
|
||||||
| SubprocessFlags.STDOUT_PIPE
|
|
||||||
})
|
|
||||||
|
|
||||||
const proc = launcher.spawnv(argv)
|
|
||||||
let stdout = new DataInputStream({
|
|
||||||
base_stream: proc.get_stdout_pipe(),
|
|
||||||
close_base_stream: true
|
|
||||||
})
|
|
||||||
|
|
||||||
return { proc, stdout }
|
|
||||||
} catch (e) {
|
|
||||||
log(`failed to spawn process: ${argv}\n\tCaused by: ${e}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function filter_selections(initial, input) {
|
|
||||||
if (input.length === 0) return []
|
|
||||||
let selections = initial.map(v => ({ ...v }))
|
|
||||||
|
|
||||||
let remove = new Array()
|
|
||||||
for (let id = 0; id < selections.length; id += 1) {
|
|
||||||
const { name, description } = selections[id]
|
|
||||||
if (name.toLowerCase().includes(input) || description.toLowerCase().includes(input)) continue
|
|
||||||
remove.push(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id of remove.reverse()) swap_remove(selections, id)
|
|
||||||
|
|
||||||
return selections
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Array<T>} array
|
|
||||||
* @param {number} index
|
|
||||||
* @returns {T | undefined}
|
|
||||||
*/
|
|
||||||
function swap_remove(array, index) {
|
|
||||||
array[index] = array[array.length - 1];
|
|
||||||
return array.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue