difftreelog
refactor merge well-known endpoints into a single crate
in: trunk
36 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2065,10 +2065,10 @@
"futures-util",
"nix",
"rand 0.10.1",
+ "remowt-endpoints",
"remowt-link-shared",
"remowt-plugin",
"remowt-polkit-shared",
- "remowt-pty",
"remowt-ui-prompt",
"serde",
"tempfile",
@@ -2090,6 +2090,7 @@
"bifrostlink-ports",
"bytes",
"camino",
+ "remowt-endpoints",
"remowt-link-shared",
"russh",
"russh-config",
@@ -2101,16 +2102,21 @@
]
[[package]]
-name = "remowt-fs"
+name = "remowt-endpoints"
version = "0.1.1"
dependencies = [
+ "anyhow",
"bifrostlink",
"bifrostlink-macros",
"camino",
+ "nix",
"serde",
"tempfile",
"thiserror",
"tokio",
+ "tracing",
+ "uuid",
+ "zbus",
]
[[package]]
@@ -2120,30 +2126,11 @@
"bifrostlink",
"bytes",
"camino",
- "remowt-fs",
- "remowt-pty",
- "remowt-systemd",
"remowt-ui-prompt",
"serde",
"serde_json",
- "thiserror",
- "tokio",
-]
-
-[[package]]
-name = "remowt-nix-daemon"
-version = "0.1.1"
-dependencies = [
- "anyhow",
- "bifrostlink",
- "bifrostlink-macros",
- "camino",
- "remowt-client",
- "serde",
"thiserror",
"tokio",
- "tracing",
- "uuid",
]
[[package]]
@@ -2171,20 +2158,6 @@
]
[[package]]
-name = "remowt-pty"
-version = "0.1.1"
-dependencies = [
- "bifrostlink",
- "bifrostlink-macros",
- "camino",
- "nix",
- "serde",
- "thiserror",
- "tokio",
- "tracing",
-]
-
-[[package]]
name = "remowt-ssh"
version = "0.1.1"
dependencies = [
@@ -2210,17 +2183,6 @@
"tracing",
"tracing-subscriber",
"uuid",
-]
-
-[[package]]
-name = "remowt-systemd"
-version = "0.1.1"
-dependencies = [
- "bifrostlink",
- "bifrostlink-macros",
- "serde",
- "thiserror",
- "zbus",
]
[[package]]
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,14 +9,12 @@
repository = "https://gitlab.delta.directory/iam/remowt"
[workspace.dependencies]
-remowt-fs = { version = "0.1.1", path = "crates/remowt-fs" }
-remowt-pty = { version = "0.1.1", path = "crates/remowt-pty" }
-remowt-systemd = { version = "0.1.1", path = "crates/remowt-systemd" }
remowt-client = { version = "0.1.1", path = "crates/remowt-client" }
remowt-polkit-shared = { version = "0.1.1", path = "crates/polkit-shared" }
remowt-link-shared = { version = "0.1.1", path = "crates/remowt-link-shared" }
remowt-plugin = { version = "0.1.1", path = "crates/remowt-plugin" }
-remowt-ui-prompt = { version = "0.1.1", path = "crates/ui-prompt" }
+remowt-ui-prompt = { version = "0.1.1", path = "crates/remowt-ui-prompt" }
+remowt-endpoints = { version = "0.1.1", path = "crates/remowt-endpoints" }
bifrostlink = "0.2.0"
bifrostlink-macros = "0.2.0"
cmds/polkit-dbus-helper/src/main.rsdiffbeforeafterboth--- a/cmds/polkit-dbus-helper/src/main.rs
+++ b/cmds/polkit-dbus-helper/src/main.rs
@@ -8,10 +8,10 @@
use nix::unistd::{setuid, Uid, User};
use pam_client::{Context, ConversationHandler, ErrorCode, Flag};
use remowt_polkit_shared::BackendRequest;
+use remowt_ui_prompt::dbus::DbusPrompterProxyBlocking;
+use remowt_ui_prompt::BlockingPrompter;
use tokio::task::{block_in_place, spawn_blocking};
use tracing::trace;
-use remowt_ui_prompt::dbus::DbusPrompterProxyBlocking;
-use remowt_ui_prompt::BlockingPrompter;
use zbus::fdo;
use zbus::message::Header;
use zbus::zvariant::OwnedValue;
cmds/remowt-agent/Cargo.tomldiffbeforeafterboth--- a/cmds/remowt-agent/Cargo.toml
+++ b/cmds/remowt-agent/Cargo.toml
@@ -17,7 +17,6 @@
rand.workspace = true
remowt-link-shared.workspace = true
remowt-plugin.workspace = true
-remowt-pty.workspace = true
serde = { workspace = true, features = ["derive"] }
tempfile.workspace = true
tokio = { workspace = true, features = [
@@ -36,3 +35,4 @@
uuid = { workspace = true, features = ["v4"] }
zbus = { workspace = true, features = ["tokio"] }
zbus_polkit = { workspace = true, features = ["tokio"] }
+remowt-endpoints.workspace = true
cmds/remowt-agent/src/helper/protocol.rsdiffbeforeafterboth--- a/cmds/remowt-agent/src/helper/protocol.rs
+++ b/cmds/remowt-agent/src/helper/protocol.rs
@@ -3,10 +3,10 @@
use anyhow::bail;
use futures::stream::Peekable;
use futures::StreamExt as _;
+use remowt_ui_prompt::Prompter;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _};
use tokio::select;
use tokio_util::codec::{FramedRead, LinesCodec};
-use remowt_ui_prompt::Prompter;
pub async fn run_conversation<R, W, P>(reader: R, mut writer: W, prompt: P) -> anyhow::Result<()>
where
cmds/remowt-agent/src/main.rsdiffbeforeafterboth--- a/cmds/remowt-agent/src/main.rs
+++ b/cmds/remowt-agent/src/main.rs
@@ -11,8 +11,8 @@
use bifrostlink_ports::stdio::from_stdio;
use bifrostlink_ports::unix_socket::from_socket;
use clap::Parser;
-use remowt_link_shared::editor::EditorEndpointsClient;
-use remowt_link_shared::{Address, BifConfig, Fs, Pty, Systemd};
+use remowt_endpoints::{fs::Fs, pty::Pty, systemd::Systemd};
+use remowt_link_shared::{editor::EditorEndpointsClient, Address, BifConfig};
use remowt_polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};
use remowt_ui_prompt::bifrost::PromptEndpointsClient;
use remowt_ui_prompt::rofi::RofiPrompter;
cmds/remowt-ssh/src/main.rsdiffbeforeafterboth--- a/cmds/remowt-ssh/src/main.rs
+++ b/cmds/remowt-ssh/src/main.rs
@@ -13,13 +13,13 @@
use remowt_client::editor::SshEditor;
use remowt_client::{AgentBundle, Remowt};
use remowt_link_shared::editor::serve_editor;
+use remowt_ui_prompt::bifrost::serve_prompts;
+use remowt_ui_prompt::rofi::RofiPrompter;
+use remowt_ui_prompt::{PrependSourcePrompter, Source};
use tokio::io::unix::AsyncFd;
use tokio::io::{AsyncRead, ReadBuf};
use tokio::signal::unix::{signal, SignalKind};
use tracing::info;
-use remowt_ui_prompt::bifrost::serve_prompts;
-use remowt_ui_prompt::rofi::RofiPrompter;
-use remowt_ui_prompt::{PrependSourcePrompter, Source};
#[derive(Parser)]
struct Opts {
crates/remowt-client/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-client/Cargo.toml
+++ b/crates/remowt-client/Cargo.toml
@@ -19,3 +19,4 @@
tokio = { workspace = true, features = ["net", "io-util", "rt", "sync", "macros", "process"] }
tracing.workspace = true
uuid = { workspace = true, features = ["v4"] }
+remowt-endpoints.workspace = true
crates/remowt-client/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-client/src/lib.rs
+++ b/crates/remowt-client/src/lib.rs
@@ -1,7 +1,7 @@
use std::collections::HashMap;
-use std::io;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
+use std::{env, io};
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use bifrostlink::declarative::RemoteEndpoints;
@@ -9,11 +9,13 @@
use bifrostlink_ports::unix_socket::from_socket;
use bytes::{Bytes, BytesMut};
use camino::{Utf8Path, Utf8PathBuf};
+use remowt_endpoints::{
+ fs::Fs,
+ pty::{Pty, PtyClient, ShellId},
+ systemd::Systemd,
+};
use remowt_link_shared::plugin::PluginEndpointsClient;
-use remowt_link_shared::{
- Address, BifConfig, ElevateEndpoints, ElevateError, Elevator, Fs, Pty, PtyClient, ShellId,
- Systemd,
-};
+use remowt_link_shared::{Address, BifConfig, ElevateEndpoints, ElevateError, Elevator};
use russh::client::{connect, Config, Handle, Handler, Msg, Session};
use russh::keys::agent::client::AgentClient;
use russh::keys::agent::AgentIdentity;
@@ -220,8 +222,8 @@
}
fn find_in_path(name: &str) -> Option<std::path::PathBuf> {
- let path = std::env::var_os("PATH")?;
- std::env::split_paths(&path)
+ let path = env::var_os("PATH")?;
+ env::split_paths(&path)
.map(|dir| dir.join(name))
.find(|p| p.is_file())
}
@@ -383,7 +385,7 @@
impl Remowt {
pub async fn connect(host: &str, bundle: &AgentBundle) -> Result<Self> {
let conf = russh_config::parse_home(host)?;
- let port = conf.host_config.port.unwrap_or(22);
+ let port = conf.host_config.port.or(conf.port).unwrap_or(22);
let hostname = conf
.host_config
.hostname
@@ -392,7 +394,7 @@
let user = conf
.user
.clone()
- .unwrap_or_else(|| std::env::var("USER").unwrap_or_else(|_| "root".to_owned()));
+ .unwrap_or_else(|| env::var("USER").unwrap_or_else(|_| "root".to_owned()));
let subs: Subs = Arc::new(Mutex::new(HashMap::new()));
let mut sess = connect(
@@ -548,7 +550,7 @@
port_from_channel(ch)
}
Transport::Local { agent_path, .. } => {
- let sock = std::env::temp_dir()
+ let sock = env::temp_dir()
.join(format!("remowt-priv-{}.sock", uuid::Uuid::new_v4()));
let _ = std::fs::remove_file(&sock);
let listener = UnixListener::bind(&sock)?;
crates/remowt-endpoints/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "remowt-endpoints"
+description = "Nix daemon proxy"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bifrostlink.workspace = true
+bifrostlink-macros.workspace = true
+camino.workspace = true
+serde = { workspace = true }
+tempfile.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["net", "io-util", "rt", "process"] }
+tracing.workspace = true
+uuid.workspace = true
+nix = { workspace = true, features = ["process", "term"] }
+zbus.workspace = true
crates/remowt-endpoints/src/fs.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/fs.rs
@@ -0,0 +1,105 @@
+use std::io::ErrorKind;
+use std::str::FromStr;
+use std::sync::Mutex;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use serde::{Deserialize, Serialize};
+use tempfile::TempDir;
+
+#[derive(Default)]
+pub struct Fs {
+ tempdirs: Mutex<Vec<TempDir>>,
+}
+
+impl Fs {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("file not found")]
+ NotFound,
+ #[error("file name/contents is not utf8")]
+ InvalidUtf8,
+ #[error("unknown fs error")]
+ Unknown,
+}
+
+#[endpoints(ns = 1)]
+impl Fs {
+ #[endpoints(id = 1)]
+ async fn read_file_tiny(&self, path: Utf8PathBuf) -> Result<Vec<u8>, Error> {
+ match tokio::fs::read(path).await {
+ Ok(v) => Ok(v),
+ Err(e) if e.kind() == ErrorKind::NotFound => Err(Error::NotFound),
+ _ => Err(Error::Unknown),
+ }
+ }
+ #[endpoints(id = 2)]
+ async fn file_exists(&self, path: Utf8PathBuf) -> bool {
+ tokio::fs::try_exists(path).await.unwrap_or(false)
+ }
+ #[endpoints(id = 3)]
+ async fn read_dir_raw(&self, path: Utf8PathBuf) -> Result<Vec<Utf8PathBuf>, Error> {
+ let mut dir = match tokio::fs::read_dir(path).await {
+ Ok(dir) => dir,
+ Err(e) if e.kind() == ErrorKind::NotFound => return Err(Error::NotFound),
+ Err(_) => return Err(Error::Unknown),
+ };
+ let mut out = Vec::new();
+ while let Ok(Some(entry)) = dir.next_entry().await {
+ let name = Utf8PathBuf::try_from(entry.file_name()).map_err(|_| Error::InvalidUtf8)?;
+ out.push(name);
+ }
+ Ok(out)
+ }
+ #[endpoints(id = 4)]
+ async fn mktemp_dir_raw(&self) -> Result<Utf8PathBuf, Error> {
+ let dir = tempfile::Builder::new()
+ .prefix("remowt.")
+ .tempdir()
+ .map_err(|_| Error::Unknown)?;
+ let mut tempdirs = self.tempdirs.lock().expect("not poisoned");
+ let path = Utf8PathBuf::try_from(dir.path().to_owned()).map_err(|_| Error::InvalidUtf8);
+ tempdirs.push(dir);
+ path
+ }
+ #[endpoints(id = 5)]
+ async fn rm_file(&self, path: Utf8PathBuf) -> Result<(), Error> {
+ match tokio::fs::remove_file(path).await {
+ Ok(()) => Ok(()),
+ Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
+ Err(_) => Err(Error::Unknown),
+ }
+ }
+}
+
+impl<C: Config> FsClient<C> {
+ pub async fn read_file_text(&self, path: impl Into<Utf8PathBuf>) -> Result<String, Error> {
+ let v = self
+ .read_file_tiny(path.into())
+ .await
+ .map_err(|_| Error::Unknown)?;
+ let v = v?;
+ String::from_utf8(v).map_err(|_| Error::InvalidUtf8)
+ }
+ pub async fn read_file_value<T: FromStr>(
+ &self,
+ path: impl Into<Utf8PathBuf>,
+ ) -> Result<Result<T, T::Err>, Error> {
+ let text = self.read_file_text(path).await?;
+ Ok(T::from_str(&text))
+ }
+ pub async fn mktemp_dir(&self) -> Result<Utf8PathBuf, Error> {
+ self.mktemp_dir_raw().await.map_err(|_| Error::Unknown)?
+ }
+ pub async fn read_dir(&self, path: impl Into<Utf8PathBuf>) -> Result<Vec<Utf8PathBuf>, Error> {
+ self.read_dir_raw(path.into())
+ .await
+ .map_err(|_| Error::Unknown)?
+ }
+}
crates/remowt-endpoints/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/lib.rs
@@ -0,0 +1,4 @@
+pub mod fs;
+pub mod nix_daemon;
+pub mod pty;
+pub mod systemd;
crates/remowt-endpoints/src/nix_daemon.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/nix_daemon.rs
@@ -0,0 +1,65 @@
+use std::process::Stdio;
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use serde::{Deserialize, Serialize};
+use std::result::Result;
+use tokio::process::Command;
+
+pub const NIX_DAEMON_SOCKET: &str = "/nix/var/nix/daemon-socket/socket";
+
+pub struct NixDaemon;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("nix daemon unavailable: {0}")]
+ DaemonUnavailable(String),
+ #[error("tunnel socket unavailable: {0}")]
+ Tunnel(String),
+}
+
+#[endpoints(ns = 4)]
+impl NixDaemon {
+ #[endpoints(id = 1)]
+ async fn connect_daemon(&self, socket: String) -> Result<(), Error> {
+ let mut daemon = tokio::net::UnixStream::connect(NIX_DAEMON_SOCKET)
+ .await
+ .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
+ let mut tunnel = tokio::net::UnixStream::connect(&socket)
+ .await
+ .map_err(|e| Error::Tunnel(e.to_string()))?;
+ tokio::spawn(async move {
+ if let Err(e) = tokio::io::copy_bidirectional(&mut daemon, &mut tunnel).await {
+ tracing::debug!("nix daemon tunnel ended: {e}");
+ }
+ });
+ Ok(())
+ }
+
+ #[endpoints(id = 2)]
+ async fn serve_store(&self, store: String, socket: String) -> Result<(), Error> {
+ let mut child = Command::new("nix-daemon")
+ .arg("--stdio")
+ .arg("--store")
+ .arg(&store)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
+ let tunnel = tokio::net::UnixStream::connect(&socket)
+ .await
+ .map_err(|e| Error::Tunnel(e.to_string()))?;
+ let mut stdin = child.stdin.take().expect("piped");
+ let mut stdout = child.stdout.take().expect("piped");
+ tokio::spawn(async move {
+ let mut tunnel = tunnel;
+ let (mut tr, mut tw) = tunnel.split();
+ let _ = tokio::join!(
+ tokio::io::copy(&mut tr, &mut stdin),
+ tokio::io::copy(&mut stdout, &mut tw),
+ );
+ let _ = child.wait().await;
+ });
+ Ok(())
+ }
+}
crates/remowt-endpoints/src/pty.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/pty.rs
@@ -0,0 +1,256 @@
+use std::collections::HashMap;
+use std::io;
+use std::os::fd::{AsRawFd, OwnedFd};
+use std::pin::Pin;
+use std::process::Stdio;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll};
+
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use camino::Utf8PathBuf;
+use nix::libc;
+use nix::pty::{openpty, OpenptyResult, Winsize};
+use serde::{Deserialize, Serialize};
+use tokio::io::unix::AsyncFd;
+use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
+use tokio::net::UnixStream;
+use tracing::{info, warn};
+
+pub type ShellId = u64;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("openpty failed: {0}")]
+ Open(String),
+ #[error("failed to spawn shell: {0}")]
+ Spawn(String),
+ #[error("failed to connect to forwarded socket: {0}")]
+ Connect(String),
+ #[error("no shell with that id")]
+ NoSuchShell,
+ #[error("resize failed: {0}")]
+ Resize(String),
+ #[error("io error: {0}")]
+ Io(String),
+}
+
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Error::Io(e.to_string())
+ }
+}
+
+#[derive(Clone, Default)]
+pub struct Pty {
+ shells: Arc<Mutex<HashMap<ShellId, OwnedFd>>>,
+ next_id: Arc<AtomicU64>,
+}
+
+impl Pty {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+#[endpoints(ns = 7)]
+impl Pty {
+ #[endpoints(id = 1)]
+ async fn open_shell(
+ &self,
+ socket_path: Utf8PathBuf,
+ term: String,
+ cols: u16,
+ rows: u16,
+ ) -> Result<ShellId, Error> {
+ let ws = Winsize {
+ ws_row: rows,
+ ws_col: cols,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ let OpenptyResult { master, slave } =
+ openpty(Some(&ws), None).map_err(|e| Error::Open(e.to_string()))?;
+
+ let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_owned());
+
+ let slave_in = slave.try_clone()?;
+ let slave_out = slave.try_clone()?;
+ let slave_err = slave;
+
+ let mut cmd = tokio::process::Command::new(&shell);
+ cmd.env("TERM", &term);
+ if let Ok(home) = std::env::var("HOME") {
+ cmd.current_dir(home);
+ }
+ cmd.stdin(Stdio::from(slave_in));
+ cmd.stdout(Stdio::from(slave_out));
+ cmd.stderr(Stdio::from(slave_err));
+ // SAFETY: only async-signal-safe calls (setsid, ioctl) before exec.
+ unsafe {
+ cmd.pre_exec(|| {
+ nix::unistd::setsid().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
+ if libc::ioctl(0, libc::TIOCSCTTY as _, 0) < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ Ok(())
+ });
+ }
+
+ let mut child = cmd.spawn().map_err(|e| Error::Spawn(e.to_string()))?;
+
+ let resize_fd = master.try_clone()?;
+ let id = self.next_id.fetch_add(1, Ordering::Relaxed);
+ self.shells
+ .lock()
+ .expect("not poisoned")
+ .insert(id, resize_fd);
+
+ let sock = match UnixStream::connect(&socket_path).await {
+ Ok(s) => s,
+ Err(e) => {
+ self.shells.lock().expect("not poisoned").remove(&id);
+ let _ = child.kill().await;
+ return Err(Error::Connect(e.to_string()));
+ }
+ };
+ let pty = AsyncPty::new(master)?;
+
+ info!(id, shell, "shell opened");
+ let shells = self.shells.clone();
+ tokio::spawn(async move {
+ let mut pty = pty;
+ let mut sock = sock;
+ if let Err(e) = tokio::io::copy_bidirectional(&mut pty, &mut sock).await {
+ warn!(id, "shell pump ended: {e}");
+ }
+ let _ = child.kill().await;
+ shells.lock().expect("not poisoned").remove(&id);
+ info!(id, "shell closed");
+ });
+
+ Ok(id)
+ }
+
+ #[endpoints(id = 2)]
+ async fn resize(&self, id: ShellId, cols: u16, rows: u16) -> Result<(), Error> {
+ let ws = libc::winsize {
+ ws_row: rows,
+ ws_col: cols,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ let shells = self.shells.lock().expect("not poisoned");
+ let fd = shells.get(&id).ok_or(Error::NoSuchShell)?;
+ // SAFETY: `fd` is a live PTY master
+ let rc = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSWINSZ as _, &ws) };
+ if rc < 0 {
+ return Err(Error::Resize(io::Error::last_os_error().to_string()));
+ }
+ Ok(())
+ }
+}
+
+struct AsyncPty {
+ fd: AsyncFd<OwnedFd>,
+}
+
+impl AsyncPty {
+ fn new(fd: OwnedFd) -> io::Result<Self> {
+ let raw = fd.as_raw_fd();
+ // SAFETY: standard F_GETFL/F_SETFL round-trip on a valid fd.
+ unsafe {
+ let flags = libc::fcntl(raw, libc::F_GETFL);
+ if flags < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ if libc::fcntl(raw, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
+ return Err(io::Error::last_os_error());
+ }
+ }
+ Ok(Self {
+ fd: AsyncFd::new(fd)?,
+ })
+ }
+}
+
+impl AsyncRead for AsyncPty {
+ fn poll_read(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll<io::Result<()>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_read_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let unfilled = buf.initialize_unfilled();
+ let res = guard.try_io(|inner| {
+ let fd = inner.get_ref().as_raw_fd();
+ // SAFETY: writing into `unfilled`'s own backing storage.
+ let n = unsafe { libc::read(fd, unfilled.as_mut_ptr().cast(), unfilled.len()) };
+ if n < 0 {
+ let err = io::Error::last_os_error();
+ if err.raw_os_error() == Some(libc::EIO) {
+ Ok(0)
+ } else {
+ Err(err)
+ }
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(Ok(n)) => {
+ buf.advance(n);
+ return Poll::Ready(Ok(()));
+ }
+ Ok(Err(e)) => return Poll::Ready(Err(e)),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+}
+
+impl AsyncWrite for AsyncPty {
+ fn poll_write(
+ self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll<io::Result<usize>> {
+ let this = self.get_mut();
+ loop {
+ let mut guard = match this.fd.poll_write_ready(cx) {
+ Poll::Ready(Ok(g)) => g,
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
+ };
+ let res = guard.try_io(|inner| {
+ let fd = inner.get_ref().as_raw_fd();
+ // SAFETY: reading from `buf` for `buf.len()` bytes.
+ let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
+ if n < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(n as usize)
+ }
+ });
+ match res {
+ Ok(r) => return Poll::Ready(r),
+ Err(_would_block) => continue,
+ }
+ }
+ }
+
+ fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ Poll::Ready(Ok(()))
+ }
+
+ fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ Poll::Ready(Ok(()))
+ }
+}
crates/remowt-endpoints/src/systemd.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-endpoints/src/systemd.rs
@@ -0,0 +1,54 @@
+use bifrostlink::declarative::endpoints;
+use bifrostlink::Config;
+use serde::{Deserialize, Serialize};
+use zbus::proxy;
+use zbus::zvariant::OwnedObjectPath;
+
+pub struct Systemd;
+
+#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
+pub enum Error {
+ #[error("systemd request failed: {0}")]
+ Failed(String),
+}
+
+#[proxy(
+ interface = "org.freedesktop.systemd1.Manager",
+ default_service = "org.freedesktop.systemd1",
+ default_path = "/org/freedesktop/systemd1"
+)]
+trait Manager {
+ fn start_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
+ fn stop_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
+}
+
+async fn manager() -> Result<ManagerProxy<'static>, Error> {
+ let conn = zbus::Connection::system()
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ ManagerProxy::new(&conn)
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))
+}
+
+#[endpoints(ns = 5)]
+impl Systemd {
+ #[endpoints(id = 1)]
+ async fn start(&self, unit: String) -> Result<(), Error> {
+ manager()
+ .await?
+ .start_unit(&unit, "replace")
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ Ok(())
+ }
+ #[endpoints(id = 2)]
+ async fn stop(&self, unit: String) -> Result<(), Error> {
+ manager()
+ .await?
+ .stop_unit(&unit, "replace")
+ .await
+ .map_err(|e| Error::Failed(e.to_string()))?;
+ Ok(())
+ }
+}
crates/remowt-fs/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-fs/Cargo.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-[package]
-name = "remowt-fs"
-description = "Filesystem endpoint for remowt/bifrostlink"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-camino = { workspace = true, features = ["serde1"] }
-serde = { workspace = true, features = ["derive"] }
-tempfile.workspace = true
-thiserror.workspace = true
-tokio = { workspace = true, features = ["fs"] }
crates/remowt-fs/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-fs/src/lib.rs
+++ /dev/null
@@ -1,105 +0,0 @@
-use std::io::ErrorKind;
-use std::str::FromStr;
-use std::sync::Mutex;
-
-use bifrostlink::declarative::endpoints;
-use bifrostlink::Config;
-use camino::Utf8PathBuf;
-use serde::{Deserialize, Serialize};
-use tempfile::TempDir;
-
-#[derive(Default)]
-pub struct Fs {
- tempdirs: Mutex<Vec<TempDir>>,
-}
-
-impl Fs {
- pub fn new() -> Self {
- Self::default()
- }
-}
-
-#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
-pub enum Error {
- #[error("file not found")]
- NotFound,
- #[error("file name/contents is not utf8")]
- InvalidUtf8,
- #[error("unknown fs error")]
- Unknown,
-}
-
-#[endpoints(ns = 1)]
-impl Fs {
- #[endpoints(id = 1)]
- async fn read_file_tiny(&self, path: Utf8PathBuf) -> Result<Vec<u8>, Error> {
- match tokio::fs::read(path).await {
- Ok(v) => Ok(v),
- Err(e) if e.kind() == ErrorKind::NotFound => Err(Error::NotFound),
- _ => Err(Error::Unknown),
- }
- }
- #[endpoints(id = 2)]
- async fn file_exists(&self, path: Utf8PathBuf) -> bool {
- tokio::fs::try_exists(path).await.unwrap_or(false)
- }
- #[endpoints(id = 3)]
- async fn read_dir_raw(&self, path: Utf8PathBuf) -> Result<Vec<Utf8PathBuf>, Error> {
- let mut dir = match tokio::fs::read_dir(path).await {
- Ok(dir) => dir,
- Err(e) if e.kind() == ErrorKind::NotFound => return Err(Error::NotFound),
- Err(_) => return Err(Error::Unknown),
- };
- let mut out = Vec::new();
- while let Ok(Some(entry)) = dir.next_entry().await {
- let name = Utf8PathBuf::try_from(entry.file_name()).map_err(|_| Error::InvalidUtf8)?;
- out.push(name);
- }
- Ok(out)
- }
- #[endpoints(id = 4)]
- async fn mktemp_dir_raw(&self) -> Result<Utf8PathBuf, Error> {
- let dir = tempfile::Builder::new()
- .prefix("remowt.")
- .tempdir()
- .map_err(|_| Error::Unknown)?;
- let mut tempdirs = self.tempdirs.lock().expect("not poisoned");
- let path = Utf8PathBuf::try_from(dir.path().to_owned()).map_err(|_| Error::InvalidUtf8);
- tempdirs.push(dir);
- path
- }
- #[endpoints(id = 5)]
- async fn rm_file(&self, path: Utf8PathBuf) -> Result<(), Error> {
- match tokio::fs::remove_file(path).await {
- Ok(()) => Ok(()),
- Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
- Err(_) => Err(Error::Unknown),
- }
- }
-}
-
-impl<C: Config> FsClient<C> {
- pub async fn read_file_text(&self, path: impl Into<Utf8PathBuf>) -> Result<String, Error> {
- let v = self
- .read_file_tiny(path.into())
- .await
- .map_err(|_| Error::Unknown)?;
- let v = v?;
- String::from_utf8(v).map_err(|_| Error::InvalidUtf8)
- }
- pub async fn read_file_value<T: FromStr>(
- &self,
- path: impl Into<Utf8PathBuf>,
- ) -> Result<Result<T, T::Err>, Error> {
- let text = self.read_file_text(path).await?;
- Ok(T::from_str(&text))
- }
- pub async fn mktemp_dir(&self) -> Result<Utf8PathBuf, Error> {
- self.mktemp_dir_raw().await.map_err(|_| Error::Unknown)?
- }
- pub async fn read_dir(&self, path: impl Into<Utf8PathBuf>) -> Result<Vec<Utf8PathBuf>, Error> {
- self.read_dir_raw(path.into())
- .await
- .map_err(|_| Error::Unknown)?
- }
-}
crates/remowt-link-shared/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-link-shared/Cargo.toml
+++ b/crates/remowt-link-shared/Cargo.toml
@@ -12,8 +12,5 @@
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["fs"] }
-remowt-fs.workspace = true
-remowt-systemd.workspace = true
remowt-ui-prompt.workspace = true
camino = { workspace = true, features = ["serde1"] }
-remowt-pty.workspace = true
crates/remowt-link-shared/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-link-shared/src/lib.rs
+++ b/crates/remowt-link-shared/src/lib.rs
@@ -21,10 +21,6 @@
pub mod plugin;
-pub use remowt_fs::{Error as FsError, Fs, FsClient};
-pub use remowt_pty::{Error as PtyError, Pty, PtyClient, ShellId};
-pub use remowt_systemd::{Error as SystemdError, Systemd, SystemdClient};
-
#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
pub enum ElevateError {
#[error("elevation failed: {0}")]
crates/remowt-nix-daemon/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-nix-daemon/Cargo.toml
+++ /dev/null
@@ -1,18 +0,0 @@
-[package]
-name = "remowt-nix-daemon"
-description = "Nix daemon proxy"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-camino.workspace = true
-remowt-client.workspace = true
-serde = { workspace = true }
-thiserror.workspace = true
-tokio = { workspace = true, features = ["net", "io-util", "rt", "process"] }
-tracing.workspace = true
-uuid.workspace = true
crates/remowt-nix-daemon/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-nix-daemon/src/lib.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-use std::process::Stdio;
-
-use bifrostlink::declarative::endpoints;
-use bifrostlink::Config;
-use serde::{Deserialize, Serialize};
-use std::result::Result;
-use tokio::process::Command;
-
-pub const NIX_DAEMON_SOCKET: &str = "/nix/var/nix/daemon-socket/socket";
-
-pub struct NixDaemon;
-
-#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
-pub enum Error {
- #[error("nix daemon unavailable: {0}")]
- DaemonUnavailable(String),
- #[error("tunnel socket unavailable: {0}")]
- Tunnel(String),
-}
-
-#[endpoints(ns = 4)]
-impl NixDaemon {
- #[endpoints(id = 1)]
- async fn connect_daemon(&self, socket: String) -> Result<(), Error> {
- let mut daemon = tokio::net::UnixStream::connect(NIX_DAEMON_SOCKET)
- .await
- .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
- let mut tunnel = tokio::net::UnixStream::connect(&socket)
- .await
- .map_err(|e| Error::Tunnel(e.to_string()))?;
- tokio::spawn(async move {
- if let Err(e) = tokio::io::copy_bidirectional(&mut daemon, &mut tunnel).await {
- tracing::debug!("nix daemon tunnel ended: {e}");
- }
- });
- Ok(())
- }
-
- #[endpoints(id = 2)]
- async fn serve_store(&self, store: String, socket: String) -> Result<(), Error> {
- let mut child = Command::new("nix-daemon")
- .arg("--stdio")
- .arg("--store")
- .arg(&store)
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .spawn()
- .map_err(|e| Error::DaemonUnavailable(e.to_string()))?;
- let tunnel = tokio::net::UnixStream::connect(&socket)
- .await
- .map_err(|e| Error::Tunnel(e.to_string()))?;
- let mut stdin = child.stdin.take().expect("piped");
- let mut stdout = child.stdout.take().expect("piped");
- tokio::spawn(async move {
- let mut tunnel = tunnel;
- let (mut tr, mut tw) = tunnel.split();
- let _ = tokio::join!(
- tokio::io::copy(&mut tr, &mut stdin),
- tokio::io::copy(&mut stdout, &mut tw),
- );
- let _ = child.wait().await;
- });
- Ok(())
- }
-}
crates/remowt-plugin/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-plugin/src/lib.rs
+++ b/crates/remowt-plugin/src/lib.rs
@@ -8,7 +8,7 @@
pub mod host;
pub use bifrostlink;
-pub use remowt_link_shared::{self, Address, BifConfig, Fs, Pty, Systemd};
+pub use remowt_link_shared::{self, Address, BifConfig};
pub fn plugin_index() -> Result<u16> {
let arg = std::env::args()
crates/remowt-pty/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-pty/Cargo.toml
+++ /dev/null
@@ -1,23 +0,0 @@
-[package]
-name = "remowt-pty"
-description = "PTY/shell endpoint for remowt"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-camino = { workspace = true, features = ["serde1"] }
-nix = { workspace = true, features = ["process", "term"] }
-serde = { workspace = true, features = ["derive"] }
-thiserror.workspace = true
-tokio = { workspace = true, features = [
- "net",
- "io-util",
- "rt",
- "macros",
- "process",
- "sync",
-] }
-tracing.workspace = true
crates/remowt-pty/src/lib.rsdiffbeforeafterbothno changes
crates/remowt-systemd/Cargo.tomldiffbeforeafterboth--- a/crates/remowt-systemd/Cargo.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-[package]
-name = "remowt-systemd"
-description = "systemd control endpoint for remowt/bifrostlink (over D-Bus)"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-serde = { workspace = true, features = ["derive"] }
-thiserror.workspace = true
-zbus = { workspace = true, features = ["tokio"] }
crates/remowt-systemd/src/lib.rsdiffbeforeafterboth--- a/crates/remowt-systemd/src/lib.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-use bifrostlink::declarative::endpoints;
-use bifrostlink::Config;
-use serde::{Deserialize, Serialize};
-use zbus::proxy;
-use zbus::zvariant::OwnedObjectPath;
-
-pub struct Systemd;
-
-#[derive(Serialize, Deserialize, Debug, thiserror::Error)]
-pub enum Error {
- #[error("systemd request failed: {0}")]
- Failed(String),
-}
-
-#[proxy(
- interface = "org.freedesktop.systemd1.Manager",
- default_service = "org.freedesktop.systemd1",
- default_path = "/org/freedesktop/systemd1"
-)]
-trait Manager {
- fn start_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
- fn stop_unit(&self, name: &str, mode: &str) -> zbus::Result<OwnedObjectPath>;
-}
-
-async fn manager() -> Result<ManagerProxy<'static>, Error> {
- let conn = zbus::Connection::system()
- .await
- .map_err(|e| Error::Failed(e.to_string()))?;
- ManagerProxy::new(&conn)
- .await
- .map_err(|e| Error::Failed(e.to_string()))
-}
-
-#[endpoints(ns = 5)]
-impl Systemd {
- #[endpoints(id = 1)]
- async fn start(&self, unit: String) -> Result<(), Error> {
- manager()
- .await?
- .start_unit(&unit, "replace")
- .await
- .map_err(|e| Error::Failed(e.to_string()))?;
- Ok(())
- }
- #[endpoints(id = 2)]
- async fn stop(&self, unit: String) -> Result<(), Error> {
- manager()
- .await?
- .stop_unit(&unit, "replace")
- .await
- .map_err(|e| Error::Failed(e.to_string()))?;
- Ok(())
- }
-}
crates/remowt-ui-prompt/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "remowt-ui-prompt"
+description = "Interactive UI prompt endpoint for remowt (D-Bus)"
+version.workspace = true
+edition = "2021"
+license.workspace = true
+
+[dependencies]
+bifrostlink.workspace = true
+bifrostlink-macros.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+thiserror.workspace = true
+tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
+tracing.workspace = true
+zbus = { workspace = true, optional = true }
+
+[features]
+default = ["dbus"]
+dbus = ["dep:zbus"]
crates/remowt-ui-prompt/src/bifrost.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/src/bifrost.rs
@@ -0,0 +1,109 @@
+use bifrostlink::{Config, Rpc};
+use bifrostlink_macros::endpoints;
+use serde::{Deserialize, Serialize};
+
+use crate::{Error, Prompter, Source};
+
+pub struct PromptEndpoints<P>(pub P);
+
+#[endpoints(ns = 2)]
+impl<P> PromptEndpoints<P>
+where
+ P: Prompter + Send + Sync + 'static,
+{
+ #[endpoints(id = 1, cancel)]
+ async fn prompt_enum(
+ &self,
+ prompt: String,
+ description: String,
+ variants: Vec<String>,
+ source: Vec<Source>,
+ ) -> Result<u32, Error> {
+ let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
+ self.0
+ .prompt_enum(&prompt, &description, &variants, &source)
+ .await
+ }
+
+ #[endpoints(id = 2, cancel)]
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: String,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<String, Error> {
+ self.0
+ .prompt_text(echo, &prompt, &description, &source)
+ .await
+ }
+
+ #[endpoints(id = 3, cancel)]
+ async fn display_text(
+ &self,
+ error: bool,
+ description: String,
+ source: Vec<Source>,
+ ) -> Result<(), Error> {
+ self.0.display_text(error, &description, &source).await
+ }
+}
+
+impl<C: Config> Prompter for PromptEndpointsClient<C>
+where
+ Error: ToString,
+{
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> crate::Result<u32> {
+ self.prompt_enum(
+ prompt.to_owned(),
+ description.to_owned(),
+ variants.iter().map(|v| (*v).to_owned()).collect(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> crate::Result<String> {
+ self.prompt_text(
+ echo,
+ prompt.to_owned(),
+ description.to_owned(),
+ source.to_vec(),
+ )
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> crate::Result<()> {
+ self.display_text(error, description.to_owned(), source.to_vec())
+ .await
+ .map_err(|e| Error::Remote(e.to_string()))?
+ }
+}
+
+pub fn serve_prompts<P, C>(rpc: &mut Rpc<C>, prompt: P)
+where
+ P: Prompter + Send + Sync + 'static,
+ C: Config,
+ C::Error: From<Error>,
+{
+ PromptEndpoints(prompt).register_endpoints(rpc);
+}
crates/remowt-ui-prompt/src/dbus.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/src/dbus.rs
@@ -0,0 +1,135 @@
+use zbus::interface;
+use zbus::{fdo, proxy};
+
+use crate::Source;
+use crate::{BlockingPrompter, Result};
+use crate::{Error, Prompter};
+
+pub struct DbusPrompterInterface<P>(pub P);
+
+#[interface(name = "lach.PolkitInputHandler")]
+impl<P: Prompter + Send + Sync + 'static> DbusPrompterInterface<P> {
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<bool> {
+ Ok(self.0.prompt_radio(prompt, description, &source).await?)
+ }
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<String> {
+ Ok(self
+ .0
+ .prompt_text(echo, prompt, description, &source)
+ .await?)
+ }
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: Vec<Source>,
+ ) -> fdo::Result<()> {
+ Ok(self.0.display_text(error, description, &source).await?)
+ }
+}
+
+#[proxy(interface = "lach.PolkitInputHandler")]
+pub trait DbusPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> fdo::Result<u32>;
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<String>;
+ async fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> fdo::Result<()>;
+}
+
+impl Prompter for DbusPrompterProxy<'_> {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self
+ .prompt_enum(prompt, description, variants, source)
+ .await?)
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source).await?)
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source).await?)
+ }
+}
+impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ Ok(self.prompt_enum(prompt, description, variants, source)?)
+ }
+
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ Ok(self.prompt_text(echo, prompt, description, source)?)
+ }
+
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ Ok(self.display_text(error, description, source)?)
+ }
+}
+
+impl From<fdo::Error> for Error {
+ fn from(value: fdo::Error) -> Self {
+ if matches!(value, fdo::Error::NoReply(_)) {
+ return Self::Cancel;
+ }
+ Self::InputError(format!("{value}"))
+ }
+}
+impl From<Error> for fdo::Error {
+ fn from(value: Error) -> Self {
+ match value {
+ Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
+ Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
+ Error::InputError(e) => fdo::Error::Failed(e),
+ }
+ }
+}
crates/remowt-ui-prompt/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/src/lib.rs
@@ -0,0 +1,201 @@
+use core::fmt;
+use std::borrow::Cow;
+use std::future::Future;
+use std::result;
+
+pub mod bifrost;
+pub mod dbus;
+pub mod rofi;
+
+#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
+pub enum Error {
+ #[error("user has cancelled input")]
+ Cancel,
+ #[error("input error: {0}")]
+ InputError(String),
+ #[error("unknown remote error: {0}")]
+ Remote(String),
+}
+
+pub type Result<T, E = Error> = result::Result<T, E>;
+
+#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
+pub struct Source(pub Cow<'static, str>);
+impl fmt::Display for Source {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "<u>{}</u>", self.0)
+ }
+}
+
+pub trait Prompter: Send + Sync {
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);
+ async { fut.await.map(|v| v == 1) }
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send;
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send;
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send;
+}
+pub trait BlockingPrompter {
+ fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {
+ self.prompt_enum(prompt, description, &["No", "Yes"], source)
+ .map(|v| v == 1)
+ }
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32>;
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String>;
+ fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;
+}
+impl<P> Prompter for &P
+where
+ P: Prompter,
+{
+ fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<bool>> + Send {
+ (*self).prompt_radio(prompt, description, source)
+ }
+
+ fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> impl Future<Output = Result<u32>> + Send {
+ (*self).prompt_enum(prompt, description, variants, source)
+ }
+
+ fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<String>> + Send {
+ (*self).prompt_text(echo, prompt, description, source)
+ }
+
+ fn display_text(
+ &self,
+ error: bool,
+ description: &str,
+ source: &[Source],
+ ) -> impl Future<Output = Result<()>> + Send {
+ (*self).display_text(error, description, source)
+ }
+}
+
+pub struct PrependSourcePrompter<P> {
+ pub prompter: P,
+ pub source: Vec<Source>,
+ pub description: String,
+}
+impl<P> PrependSourcePrompter<P> {
+ fn source(&self, input: &[Source]) -> Vec<Source> {
+ let mut out = self.source.clone();
+ out.extend(input.iter().cloned());
+ out
+ }
+ fn description(&self, input: &str) -> String {
+ if self.description.is_empty() {
+ input.to_owned()
+ } else if input.is_empty() {
+ self.description.to_owned()
+ } else {
+ format!("{input}\n\n{}", self.description)
+ }
+ }
+}
+impl<P> Prompter for PrependSourcePrompter<P>
+where
+ P: Prompter + Sync,
+{
+ async fn prompt_radio(
+ &self,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<bool> {
+ self.prompter
+ .prompt_radio(prompt, &self.description(description), &self.source(source))
+ .await
+ }
+
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ self.prompter
+ .prompt_enum(
+ prompt,
+ &self.description(description),
+ variants,
+ &self.source(source),
+ )
+ .await
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ self.prompter
+ .prompt_text(
+ echo,
+ prompt,
+ &self.description(description),
+ &self.source(source),
+ )
+ .await
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ self.prompter
+ .display_text(error, &self.description(description), &self.source(source))
+ .await
+ }
+}
crates/remowt-ui-prompt/src/rofi.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/remowt-ui-prompt/src/rofi.rs
@@ -0,0 +1,208 @@
+use std::process::Stdio;
+
+use tokio::io::AsyncWriteExt;
+use tokio::process::Command;
+use tracing::trace;
+
+use crate::{Error, Prompter, Result, Source};
+
+#[derive(Clone)]
+pub struct RofiPrompter;
+
+fn fixup_prompt(prompt: &str) -> &str {
+ // Rofi always appends such suffix
+ prompt.strip_suffix(": ").unwrap_or(prompt)
+}
+
+fn rofi_command() -> Command {
+ Command::new(option_env!("ROFI").unwrap_or("rofi"))
+}
+
+impl Prompter for RofiPrompter {
+ async fn prompt_enum(
+ &self,
+ prompt: &str,
+ description: &str,
+ variants: &[&str],
+ source: &[Source],
+ ) -> Result<u32> {
+ trace!("rofi radio");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args([
+ "-dmenu",
+ "-mesg",
+ &mesg,
+ "-sync",
+ "-only-match",
+ "-p",
+ fixup_prompt(prompt),
+ "-format",
+ "i",
+ "-markup-rows",
+ ]);
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ let mut stdin = child.stdin.take().expect("stdin is piped");
+ for var in variants {
+ stdin
+ .write_all(var.replace('\n', " ").as_bytes())
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ stdin
+ .write_all(b"\n")
+ .await
+ .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
+ }
+ // write_all already flushes, just to be sure.
+ let _ = stdin.flush().await;
+ drop(stdin);
+
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
+
+ let id: u32 = String::from_utf8(stdout)
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?
+ .parse()
+ .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;
+ if id as usize >= variants.len() {
+ return Err(Error::InputError("invalid rofi response".to_owned()));
+ }
+
+ Ok(id)
+ }
+
+ async fn prompt_text(
+ &self,
+ echo: bool,
+ prompt: &str,
+ description: &str,
+ source: &[Source],
+ ) -> Result<String> {
+ trace!("rofi text");
+ let mut cmd = rofi_command();
+ let mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Requested on ",);
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);
+ if !echo {
+ cmd.arg("-password");
+ }
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::piped());
+ cmd.kill_on_drop(true);
+ let child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ let out = child
+ .wait_with_output()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+ let stdout = out
+ .stdout
+ .strip_suffix(b"\n")
+ .unwrap_or(&out.stdout)
+ .to_owned();
+
+ Ok(String::from_utf8_lossy(&stdout).to_string())
+ }
+
+ async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
+ trace!("rofi display");
+ let mut cmd = rofi_command();
+ let mut mesg = if source.is_empty() {
+ description.to_owned()
+ } else {
+ let mut out = format!("{description}\n\n<b>Coming from ",);
+ for s in source.iter() {
+ out.push_str(&s.to_string());
+ }
+ out.push_str("</b>");
+ out
+ };
+ if error {
+ mesg.insert_str(0, "<span color=\"red\">");
+ mesg.push_str("</span>");
+ }
+ cmd.args(["-e", &mesg, "-markup"]);
+ cmd.stdin(Stdio::null());
+ cmd.stdout(Stdio::null());
+ cmd.kill_on_drop(true);
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
+
+ child
+ .wait()
+ .await
+ .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::borrow::Cow;
+
+ use crate::rofi::RofiPrompter;
+ use crate::{PrependSourcePrompter, Prompter as _, Source};
+
+ // #[tokio::test]
+ #[tokio::test]
+ #[ignore = "interactive"]
+ async fn test() {
+ let prompter = PrependSourcePrompter {
+ prompter: RofiPrompter,
+ description: "test".to_owned(),
+ source: vec![Source(Cow::Borrowed("ssh"))],
+ };
+ prompter
+ .prompt_radio("Enable", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .prompt_text(false, "Password", "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ prompter
+ .display_text(true, "Polkit needs access", &[])
+ .await
+ .expect("rofi");
+ }
+}
crates/ui-prompt/Cargo.tomldiffbeforeafterboth--- a/crates/ui-prompt/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[package]
-name = "remowt-ui-prompt"
-description = "Interactive UI prompt endpoint for remowt (D-Bus)"
-version.workspace = true
-edition = "2021"
-license.workspace = true
-
-[dependencies]
-bifrostlink.workspace = true
-bifrostlink-macros.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-thiserror.workspace = true
-tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
-tracing.workspace = true
-zbus = { workspace = true, optional = true }
-
-[features]
-default = ["dbus"]
-dbus = ["dep:zbus"]
crates/ui-prompt/src/bifrost.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/bifrost.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-use bifrostlink::{Config, Rpc};
-use bifrostlink_macros::endpoints;
-use serde::{Deserialize, Serialize};
-
-use crate::{Error, Prompter, Source};
-
-pub struct PromptEndpoints<P>(pub P);
-
-#[endpoints(ns = 2)]
-impl<P> PromptEndpoints<P>
-where
- P: Prompter + Send + Sync + 'static,
-{
- #[endpoints(id = 1, cancel)]
- async fn prompt_enum(
- &self,
- prompt: String,
- description: String,
- variants: Vec<String>,
- source: Vec<Source>,
- ) -> Result<u32, Error> {
- let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
- self.0
- .prompt_enum(&prompt, &description, &variants, &source)
- .await
- }
-
- #[endpoints(id = 2, cancel)]
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: String,
- description: String,
- source: Vec<Source>,
- ) -> Result<String, Error> {
- self.0
- .prompt_text(echo, &prompt, &description, &source)
- .await
- }
-
- #[endpoints(id = 3, cancel)]
- async fn display_text(
- &self,
- error: bool,
- description: String,
- source: Vec<Source>,
- ) -> Result<(), Error> {
- self.0.display_text(error, &description, &source).await
- }
-}
-
-impl<C: Config> Prompter for PromptEndpointsClient<C>
-where
- Error: ToString,
-{
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> crate::Result<u32> {
- self.prompt_enum(
- prompt.to_owned(),
- description.to_owned(),
- variants.iter().map(|v| (*v).to_owned()).collect(),
- source.to_vec(),
- )
- .await
- .map_err(|e| Error::Remote(e.to_string()))?
- }
-
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> crate::Result<String> {
- self.prompt_text(
- echo,
- prompt.to_owned(),
- description.to_owned(),
- source.to_vec(),
- )
- .await
- .map_err(|e| Error::Remote(e.to_string()))?
- }
-
- async fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> crate::Result<()> {
- self.display_text(error, description.to_owned(), source.to_vec())
- .await
- .map_err(|e| Error::Remote(e.to_string()))?
- }
-}
-
-pub fn serve_prompts<P, C>(rpc: &mut Rpc<C>, prompt: P)
-where
- P: Prompter + Send + Sync + 'static,
- C: Config,
- C::Error: From<Error>,
-{
- PromptEndpoints(prompt).register_endpoints(rpc);
-}
crates/ui-prompt/src/dbus.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/dbus.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-use zbus::interface;
-use zbus::{fdo, proxy};
-
-use crate::Source;
-use crate::{BlockingPrompter, Result};
-use crate::{Error, Prompter};
-
-pub struct DbusPrompterInterface<P>(pub P);
-
-#[interface(name = "lach.PolkitInputHandler")]
-impl<P: Prompter + Send + Sync + 'static> DbusPrompterInterface<P> {
- async fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<bool> {
- Ok(self.0.prompt_radio(prompt, description, &source).await?)
- }
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<String> {
- Ok(self
- .0
- .prompt_text(echo, prompt, description, &source)
- .await?)
- }
- async fn display_text(
- &self,
- error: bool,
- description: &str,
- source: Vec<Source>,
- ) -> fdo::Result<()> {
- Ok(self.0.display_text(error, description, &source).await?)
- }
-}
-
-#[proxy(interface = "lach.PolkitInputHandler")]
-pub trait DbusPrompter {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> fdo::Result<u32>;
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> fdo::Result<String>;
- async fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> fdo::Result<()>;
-}
-
-impl Prompter for DbusPrompterProxy<'_> {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- Ok(self
- .prompt_enum(prompt, description, variants, source)
- .await?)
- }
-
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- Ok(self.prompt_text(echo, prompt, description, source).await?)
- }
-
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- Ok(self.display_text(error, description, source).await?)
- }
-}
-impl BlockingPrompter for DbusPrompterProxyBlocking<'_> {
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- Ok(self.prompt_enum(prompt, description, variants, source)?)
- }
-
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- Ok(self.prompt_text(echo, prompt, description, source)?)
- }
-
- fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- Ok(self.display_text(error, description, source)?)
- }
-}
-
-impl From<fdo::Error> for Error {
- fn from(value: fdo::Error) -> Self {
- if matches!(value, fdo::Error::NoReply(_)) {
- return Self::Cancel;
- }
- Self::InputError(format!("{value}"))
- }
-}
-impl From<Error> for fdo::Error {
- fn from(value: Error) -> Self {
- match value {
- Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
- Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
- Error::InputError(e) => fdo::Error::Failed(e),
- }
- }
-}
crates/ui-prompt/src/lib.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/lib.rs
+++ /dev/null
@@ -1,201 +0,0 @@
-use core::fmt;
-use std::borrow::Cow;
-use std::future::Future;
-use std::result;
-
-pub mod bifrost;
-pub mod dbus;
-pub mod rofi;
-
-#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
-pub enum Error {
- #[error("user has cancelled input")]
- Cancel,
- #[error("input error: {0}")]
- InputError(String),
- #[error("unknown remote error: {0}")]
- Remote(String),
-}
-
-pub type Result<T, E = Error> = result::Result<T, E>;
-
-#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
-#[derive(serde::Serialize, serde::Deserialize, Clone)]
-pub struct Source(pub Cow<'static, str>);
-impl fmt::Display for Source {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "<u>{}</u>", self.0)
- }
-}
-
-pub trait Prompter: Send + Sync {
- fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<bool>> + Send {
- let fut = self.prompt_enum(prompt, description, &["No", "Yes"], source);
- async { fut.await.map(|v| v == 1) }
- }
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> impl Future<Output = Result<u32>> + Send;
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<String>> + Send;
- fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<()>> + Send;
-}
-pub trait BlockingPrompter {
- fn prompt_radio(&self, prompt: &str, description: &str, source: &[Source]) -> Result<bool> {
- self.prompt_enum(prompt, description, &["No", "Yes"], source)
- .map(|v| v == 1)
- }
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32>;
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String>;
- fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()>;
-}
-impl<P> Prompter for &P
-where
- P: Prompter,
-{
- fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<bool>> + Send {
- (*self).prompt_radio(prompt, description, source)
- }
-
- fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> impl Future<Output = Result<u32>> + Send {
- (*self).prompt_enum(prompt, description, variants, source)
- }
-
- fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<String>> + Send {
- (*self).prompt_text(echo, prompt, description, source)
- }
-
- fn display_text(
- &self,
- error: bool,
- description: &str,
- source: &[Source],
- ) -> impl Future<Output = Result<()>> + Send {
- (*self).display_text(error, description, source)
- }
-}
-
-pub struct PrependSourcePrompter<P> {
- pub prompter: P,
- pub source: Vec<Source>,
- pub description: String,
-}
-impl<P> PrependSourcePrompter<P> {
- fn source(&self, input: &[Source]) -> Vec<Source> {
- let mut out = self.source.clone();
- out.extend(input.iter().cloned());
- out
- }
- fn description(&self, input: &str) -> String {
- if self.description.is_empty() {
- input.to_owned()
- } else if input.is_empty() {
- self.description.to_owned()
- } else {
- format!("{input}\n\n{}", self.description)
- }
- }
-}
-impl<P> Prompter for PrependSourcePrompter<P>
-where
- P: Prompter + Sync,
-{
- async fn prompt_radio(
- &self,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<bool> {
- self.prompter
- .prompt_radio(prompt, &self.description(description), &self.source(source))
- .await
- }
-
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- self.prompter
- .prompt_enum(
- prompt,
- &self.description(description),
- variants,
- &self.source(source),
- )
- .await
- }
-
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- self.prompter
- .prompt_text(
- echo,
- prompt,
- &self.description(description),
- &self.source(source),
- )
- .await
- }
-
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- self.prompter
- .display_text(error, &self.description(description), &self.source(source))
- .await
- }
-}
crates/ui-prompt/src/rofi.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/rofi.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use std::process::Stdio;
-
-use tokio::io::AsyncWriteExt;
-use tokio::process::Command;
-use tracing::trace;
-
-use crate::{Error, Prompter, Result, Source};
-
-#[derive(Clone)]
-pub struct RofiPrompter;
-
-fn fixup_prompt(prompt: &str) -> &str {
- // Rofi always appends such suffix
- prompt.strip_suffix(": ").unwrap_or(prompt)
-}
-
-fn rofi_command() -> Command {
- Command::new(option_env!("ROFI").unwrap_or("rofi"))
-}
-
-impl Prompter for RofiPrompter {
- async fn prompt_enum(
- &self,
- prompt: &str,
- description: &str,
- variants: &[&str],
- source: &[Source],
- ) -> Result<u32> {
- trace!("rofi radio");
- let mut cmd = rofi_command();
- let mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Requested on ",);
- for (i, s) in source.iter().enumerate() {
- if i != 0 {
- out.push_str(" -> ");
- }
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- cmd.args([
- "-dmenu",
- "-mesg",
- &mesg,
- "-sync",
- "-only-match",
- "-p",
- fixup_prompt(prompt),
- "-format",
- "i",
- "-markup-rows",
- ]);
- cmd.stdin(Stdio::piped());
- cmd.stdout(Stdio::piped());
- cmd.kill_on_drop(true);
- let mut child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
-
- let mut stdin = child.stdin.take().expect("stdin is piped");
- for var in variants {
- stdin
- .write_all(var.replace('\n', " ").as_bytes())
- .await
- .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
- stdin
- .write_all(b"\n")
- .await
- .map_err(|e| Error::InputError(format!("failed to write rofi variants: {e}")))?;
- }
- // write_all already flushes, just to be sure.
- let _ = stdin.flush().await;
- drop(stdin);
-
- let out = child
- .wait_with_output()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
- let stdout = out
- .stdout
- .strip_suffix(b"\n")
- .unwrap_or(&out.stdout)
- .to_owned();
-
- let id: u32 = String::from_utf8(stdout)
- .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?
- .parse()
- .map_err(|e| Error::InputError(format!("rofi produced invalid output: {e}")))?;
- if id as usize >= variants.len() {
- return Err(Error::InputError("invalid rofi response".to_owned()));
- }
-
- Ok(id)
- }
-
- async fn prompt_text(
- &self,
- echo: bool,
- prompt: &str,
- description: &str,
- source: &[Source],
- ) -> Result<String> {
- trace!("rofi text");
- let mut cmd = rofi_command();
- let mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Requested on ",);
- for (i, s) in source.iter().enumerate() {
- if i != 0 {
- out.push_str(" -> ");
- }
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- cmd.args(["-dmenu", "-mesg", &mesg, "-p", fixup_prompt(prompt)]);
- if !echo {
- cmd.arg("-password");
- }
- cmd.stdin(Stdio::null());
- cmd.stdout(Stdio::piped());
- cmd.kill_on_drop(true);
- let child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
-
- let out = child
- .wait_with_output()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
- let stdout = out
- .stdout
- .strip_suffix(b"\n")
- .unwrap_or(&out.stdout)
- .to_owned();
-
- Ok(String::from_utf8_lossy(&stdout).to_string())
- }
-
- async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
- trace!("rofi display");
- let mut cmd = rofi_command();
- let mut mesg = if source.is_empty() {
- description.to_owned()
- } else {
- let mut out = format!("{description}\n\n<b>Coming from ",);
- for s in source.iter() {
- out.push_str(&s.to_string());
- }
- out.push_str("</b>");
- out
- };
- if error {
- mesg.insert_str(0, "<span color=\"red\">");
- mesg.push_str("</span>");
- }
- cmd.args(["-e", &mesg, "-markup"]);
- cmd.stdin(Stdio::null());
- cmd.stdout(Stdio::null());
- cmd.kill_on_drop(true);
- let mut child = cmd
- .spawn()
- .map_err(|e| Error::InputError(format!("failed to spawn rofi: {e}")))?;
-
- child
- .wait()
- .await
- .map_err(|e| Error::InputError(format!("failed to wait for rofi: {e}")))?;
-
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::borrow::Cow;
-
- use crate::rofi::RofiPrompter;
- use crate::{PrependSourcePrompter, Prompter as _, Source};
-
- // #[tokio::test]
- #[tokio::test]
- #[ignore = "interactive"]
- async fn test() {
- let prompter = PrependSourcePrompter {
- prompter: RofiPrompter,
- description: "test".to_owned(),
- source: vec![Source(Cow::Borrowed("ssh"))],
- };
- prompter
- .prompt_radio("Enable", "Polkit needs access", &[])
- .await
- .expect("rofi");
- prompter
- .prompt_text(false, "Password", "Polkit needs access", &[])
- .await
- .expect("rofi");
- prompter
- .display_text(true, "Polkit needs access", &[])
- .await
- .expect("rofi");
- }
-}