difftreelog
feat functional polkit prompt
in: trunk
7 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -883,6 +883,7 @@
name = "polkit-shared"
version = "0.1.0"
dependencies = [
+ "nix",
"serde",
"zbus",
]
cmds/polkit-backend/src/main.rsdiffbeforeafterboth1use std::collections::{HashMap, HashSet};2use std::ffi::{CStr, CString};3use std::future::pending;4use std::sync::LazyLock;56use anyhow::Context as _;7use clap::Parser;8use nix::unistd::{setuid, Uid, User};9use pam_client::{Context, ConversationHandler, ErrorCode, Flag};10use polkit_shared::BackendRequest;11use tokio::runtime::Handle;12use tokio::task::{block_in_place, spawn_blocking};13use tracing::trace;14use ui_prompt::dbus::DbusPrompterProxyBlocking;15use ui_prompt::{BlockingPrompter, Prompter};16use zbus::fdo;17use zbus::message::Header;18use zbus::zvariant::OwnedValue;19use zbus::{blocking, interface, proxy, Connection};2021struct Helper {22 connection: Connection,23 blocking_connection: blocking::Connection,24}2526static ALLOWED_ENVIRONMENT: LazyLock<HashSet<&str>> = LazyLock::new(|| {27 [28 // pam ssh agent auth29 "SSH_AUTH_SOCK",30 // ssh itself provides this when running PAM31 "SSH_AUTH_INFO_0",32 // contains user which ran sudo33 "SUDO_USER",34 ]35 .into_iter()36 .collect()37});3839struct Conversation<P>(P);40impl<P: BlockingPrompter> Conversation<P> {41 fn prompt_inner(&self, echo: bool, prompt: &CStr) -> Result<CString, ErrorCode> {42 trace!("do prompt");43 let out = self44 .045 .prompt_text(46 echo,47 &prompt.to_string_lossy(),48 "Polkit prompt request",49 &[],50 )51 .map_err(|e| {52 trace!("prompt error: {e}");53 ErrorCode::CONV_ERR54 })?;55 CString::new(out).map_err(|_| ErrorCode::CONV_AGAIN)56 }57 fn text_inner(&self, error: bool, msg: &CStr) {58 trace!("do text");59 let msg = msg.to_string_lossy();60 let _ = self.0.display_text(error, &msg, &[]);61 }62}63impl<P: BlockingPrompter> ConversationHandler for Conversation<P> {64 fn prompt_echo_on(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {65 self.prompt_inner(true, prompt)66 }6768 fn prompt_echo_off(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {69 self.prompt_inner(false, prompt)70 }7172 fn text_info(&mut self, msg: &CStr) {73 self.text_inner(false, msg)74 }7576 fn error_msg(&mut self, msg: &CStr) {77 self.text_inner(true, msg)78 }7980 fn radio_prompt(&mut self, prompt: &CStr) -> Result<bool, ErrorCode> {81 let prompt = prompt.to_string_lossy();82 let result = self83 .084 .prompt_radio(&prompt, "Polkit prompt request", &[])85 .map_err(|_| ErrorCode::CONV_ERR)?;86 Ok(result)87 }88}8990#[proxy(91 default_service = "org.freedesktop.DBus",92 default_path = "/org/freedesktop/DBus"93)]94trait DBus {95 fn get_connection_credentials(&self, body: &str) -> zbus::Result<HashMap<String, OwnedValue>>;96}9798#[interface(name = "lach.PolkitHelper")]99impl Helper {100 async fn init_conversation(101 &self,102 request: BackendRequest,103 #[zbus(header)] hdr: Header<'_>,104 ) -> fdo::Result<()> {105 let Some(sender) = hdr.sender().map(|v| v.to_owned()) else {106 trace!("missing sender");107 return Err(fdo::Error::AuthFailed("missing sender".to_owned()));108 };109110 let dbus = DBusProxy::new(&self.connection).await?;111112 // TOCTOU: sender might be already disconnected, and there might be another113 // user with different user id here, but does it matters?114 let reply = dbus.get_connection_credentials(&sender).await?;115 let uid: u32 = (&reply["UnixUserID"]).try_into().unwrap();116117 let blocking_connection = self.blocking_connection.clone();118 let thread_result: fdo::Result<()> = block_in_place(move || {119 trace!("find user");120 let user = User::from_uid(Uid::from_raw(uid))121 .map_err(|_| fdo::Error::AuthFailed("error querying user".to_owned()))?122 .ok_or_else(|| fdo::Error::AuthFailed("uid not found".to_owned()))?;123124 let responder = DbusPrompterProxyBlocking::new(125 &blocking_connection,126 sender,127 request.prompter_path,128 )?;129 let conversation = Conversation(responder);130 trace!("run context for {}", &user.name);131 let mut ctx = Context::new(132 // TODO: Should another scope be used?133 "login",134 Some(&user.name),135 conversation,136 )137 .map_err(|_| fdo::Error::Failed("pam context init failed".to_owned()))?;138139 trace!("fill env");140 for (k, v) in request.environment {141 if k.contains('=') || !ALLOWED_ENVIRONMENT.contains(k.as_str()) {142 continue;143 }144 let _ = ctx.putenv(format!("{k}={v}"));145 }146147 trace!("authenticate");148 ctx.authenticate(Flag::NONE)149 .map_err(|_| fdo::Error::AuthFailed("pam authentication failed".to_owned()))?;150151 trace!("acct mgmt");152 ctx.acct_mgmt(Flag::NONE)153 .map_err(|_| fdo::Error::AuthFailed("pam acct mgmt failed".to_owned()))?;154155 Ok(())156 });157158 thread_result?;159160 trace!("respond");161 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&self.connection).await?;162163 let identity_details = request164 .identity165 .details166 .iter()167 .map(|(k, v)| (k.as_str(), (**v).try_clone().expect("success")))168 .collect::<HashMap<_, _>>();169 proxy170 .authentication_agent_response2(171 uid,172 &request.cookie,173 &zbus_polkit::policykit1::Identity {174 identity_kind: &request.identity.kind,175 identity_details: &identity_details,176 },177 )178 .await?;179 Ok(())180 }181}182183const OBJ_PATH: &str = "/lach/PolkitHelper";184185#[derive(Parser)]186struct Opts {187 /// Not recommended: start as a session connection, then use escalation188 /// to respond to polkit requests.189 #[arg(long)]190 session: bool,191}192193#[tokio::main]194async fn main() -> anyhow::Result<()> {195 tracing_subscriber::fmt::init();196 let opts = Opts::parse();197 let connection = if opts.session {198 Connection::session().await199 } else {200 Connection::system().await201 }202 .context("failed to open connection")?;203204 let session = opts.session;205 let blocking_connection: anyhow::Result<blocking::Connection> = spawn_blocking(move || {206 Ok(if session {207 blocking::Connection::session()?208 } else {209 blocking::Connection::system()?210 })211 })212 .await?;213 let blocking_connection = blocking_connection.context("failed to open blocking connection")?;214215 if opts.session {216 setuid(Uid::from_raw(0))217 .context("polkit-backend needs to be suid if run in session mode")?;218 }219220 connection221 .object_server()222 .at(223 OBJ_PATH,224 Helper {225 connection: connection.clone(),226 blocking_connection,227 },228 )229 .await230 .context("failed listen path")?;231232 connection233 .request_name("lach.polkit.helper1")234 .await235 .context("failed to request name")?;236237 pending().await238}1use std::collections::{HashMap, HashSet};2use std::ffi::{CStr, CString};3use std::future::pending;4use std::sync::LazyLock;56use anyhow::Context as _;7use clap::Parser;8use nix::unistd::{setuid, Uid, User};9use pam_client::{Context, ConversationHandler, ErrorCode, Flag};10use polkit_shared::BackendRequest;11use tokio::runtime::Handle;12use tokio::task::{block_in_place, spawn_blocking};13use tracing::trace;14use ui_prompt::dbus::DbusPrompterProxyBlocking;15use ui_prompt::{BlockingPrompter, Prompter};16use zbus::fdo;17use zbus::message::Header;18use zbus::zvariant::OwnedValue;19use zbus::{blocking, interface, proxy, Connection};2021struct Helper {22 connection: Connection,23 blocking_connection: blocking::Connection,24}2526static ALLOWED_ENVIRONMENT: LazyLock<HashSet<&str>> = LazyLock::new(|| {27 [28 // pam ssh agent auth29 "SSH_AUTH_SOCK",30 // ssh itself provides this when running PAM31 "SSH_AUTH_INFO_0",32 // contains user which ran sudo33 "SUDO_USER",34 ]35 .into_iter()36 .collect()37});3839struct Conversation<P>(P);40impl<P: BlockingPrompter> Conversation<P> {41 fn prompt_inner(&self, echo: bool, prompt: &CStr) -> Result<CString, ErrorCode> {42 trace!("do prompt");43 let out = self44 .045 .prompt_text(46 echo,47 &prompt.to_string_lossy(),48 "PAM prompt request",49 &[],50 )51 .map_err(|e| {52 trace!("prompt error: {e}");53 ErrorCode::CONV_ERR54 })?;55 CString::new(out).map_err(|_| ErrorCode::CONV_AGAIN)56 }57 fn text_inner(&self, error: bool, msg: &CStr) {58 trace!("do text");59 let msg = msg.to_string_lossy();60 let _ = self.0.display_text(error, &msg, &[]);61 }62}63impl<P: BlockingPrompter> ConversationHandler for Conversation<P> {64 fn prompt_echo_on(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {65 self.prompt_inner(true, prompt)66 }6768 fn prompt_echo_off(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {69 self.prompt_inner(false, prompt)70 }7172 fn text_info(&mut self, msg: &CStr) {73 self.text_inner(false, msg)74 }7576 fn error_msg(&mut self, msg: &CStr) {77 self.text_inner(true, msg)78 }7980 fn radio_prompt(&mut self, prompt: &CStr) -> Result<bool, ErrorCode> {81 let prompt = prompt.to_string_lossy();82 let result = self83 .084 .prompt_radio(&prompt, "PAM prompt request", &[])85 .map_err(|_| ErrorCode::CONV_ERR)?;86 Ok(result)87 }88}8990#[proxy(91 default_service = "org.freedesktop.DBus",92 default_path = "/org/freedesktop/DBus"93)]94trait DBus {95 fn get_connection_credentials(&self, body: &str) -> zbus::Result<HashMap<String, OwnedValue>>;96}9798#[interface(name = "lach.PolkitHelper")]99impl Helper {100 async fn init_conversation(101 &self,102 request: BackendRequest,103 #[zbus(header)] hdr: Header<'_>,104 ) -> fdo::Result<()> {105 let Some(sender) = hdr.sender().map(|v| v.to_owned()) else {106 trace!("missing sender");107 return Err(fdo::Error::AuthFailed("missing sender".to_owned()));108 };109110 let dbus = DBusProxy::new(&self.connection).await?;111112 // TOCTOU: sender might be already disconnected, and there might be another113 // user with different user id here, but does it matters?114 let reply = dbus.get_connection_credentials(&sender).await?;115 let uid: u32 = (&reply["UnixUserID"]).try_into().unwrap();116117 let blocking_connection = self.blocking_connection.clone();118 let thread_result: fdo::Result<()> = block_in_place(move || {119 trace!("find user");120 let user = User::from_uid(Uid::from_raw(uid))121 .map_err(|_| fdo::Error::AuthFailed("error querying user".to_owned()))?122 .ok_or_else(|| fdo::Error::AuthFailed("uid not found".to_owned()))?;123124 let responder = DbusPrompterProxyBlocking::new(125 &blocking_connection,126 sender,127 request.prompter_path,128 )?;129 let conversation = Conversation(responder);130 trace!("run context for {}", &user.name);131 let mut ctx = Context::new(132 // TODO: Should another scope be used?133 "login",134 Some(&user.name),135 conversation,136 )137 .map_err(|_| fdo::Error::Failed("pam context init failed".to_owned()))?;138139 trace!("fill env");140 for (k, v) in request.environment {141 if k.contains('=') || !ALLOWED_ENVIRONMENT.contains(k.as_str()) {142 continue;143 }144 let _ = ctx.putenv(format!("{k}={v}"));145 }146147 trace!("authenticate");148 ctx.authenticate(Flag::NONE)149 .map_err(|_| fdo::Error::AuthFailed("pam authentication failed".to_owned()))?;150151 trace!("acct mgmt");152 ctx.acct_mgmt(Flag::NONE)153 .map_err(|_| fdo::Error::AuthFailed("pam acct mgmt failed".to_owned()))?;154155 Ok(())156 });157158 thread_result?;159160 trace!("respond");161 let proxy = zbus_polkit::policykit1::AuthorityProxy::new(&self.connection).await?;162163 let identity_details = request164 .identity165 .details166 .iter()167 .map(|(k, v)| (k.as_str(), (**v).try_clone().expect("success")))168 .collect::<HashMap<_, _>>();169 proxy170 .authentication_agent_response2(171 uid,172 &request.cookie,173 &zbus_polkit::policykit1::Identity {174 identity_kind: &request.identity.kind,175 identity_details: &identity_details,176 },177 )178 .await?;179 Ok(())180 }181}182183const OBJ_PATH: &str = "/lach/PolkitHelper";184185#[derive(Parser)]186struct Opts {187 /// Not recommended: start as a session connection, then use escalation188 /// to respond to polkit requests.189 #[arg(long)]190 session: bool,191}192193#[tokio::main]194async fn main() -> anyhow::Result<()> {195 tracing_subscriber::fmt::init();196 let opts = Opts::parse();197 let connection = if opts.session {198 Connection::session().await199 } else {200 Connection::system().await201 }202 .context("failed to open connection")?;203204 let session = opts.session;205 let blocking_connection: anyhow::Result<blocking::Connection> = spawn_blocking(move || {206 Ok(if session {207 blocking::Connection::session()?208 } else {209 blocking::Connection::system()?210 })211 })212 .await?;213 let blocking_connection = blocking_connection.context("failed to open blocking connection")?;214215 if opts.session {216 setuid(Uid::from_raw(0))217 .context("polkit-backend needs to be suid if run in session mode")?;218 }219220 connection221 .object_server()222 .at(223 OBJ_PATH,224 Helper {225 connection: connection.clone(),226 blocking_connection,227 },228 )229 .await230 .context("failed listen path")?;231232 connection233 .request_name("lach.polkit.helper1")234 .await235 .context("failed to request name")?;236237 pending().await238}cmds/remowt-agent/src/main.rsdiffbeforeafterboth--- a/cmds/remowt-agent/src/main.rs
+++ b/cmds/remowt-agent/src/main.rs
@@ -1,19 +1,20 @@
-use std::collections::HashMap;
+use std::borrow::Cow;
+use std::collections::{BTreeMap, HashMap};
use std::io::{stdout, Write};
use std::marker::PhantomData;
use std::sync::{Mutex, RwLock};
use std::{future, process};
use clap::Parser;
-use polkit_shared::{BackendRequest, Identity};
+use polkit_shared::{emphasize, BackendRequest, Identity, PidDisplay};
use tokio::runtime::Handle;
use tokio::task::{AbortHandle, JoinHandle, LocalSet};
use tracing::trace;
use ui_prompt::dbus::DbusPrompterInterface;
use ui_prompt::rofi::RofiPrompter;
-use ui_prompt::Prompter;
+use ui_prompt::{PrependSourcePrompter, Prompter, Source};
use zbus::zvariant::{OwnedValue, Str};
-use zbus::ObjectServer;
+use zbus::{fdo, ObjectServer};
use zbus::{interface, proxy, Connection};
use zbus_polkit::policykit1::Subject;
@@ -81,10 +82,11 @@
action_id: String,
message: String,
icon_name: String,
- details: HashMap<String, String>,
+ mut details: BTreeMap<String, String>,
cookie: String,
identities: Vec<Identity>,
) -> zbus::fdo::Result<()> {
+ use std::fmt::Write;
trace!("begin auth");
let task = {
let connection = self.connection.clone();
@@ -92,7 +94,61 @@
let cookie = cookie.clone();
tokio::task::spawn(async move {
trace!("conversation task");
- let prompter = TemporaryPrompterInterface::new(connection, RofiPrompter).await;
+ let mut description = format!("{message}\n\n<b>Action id:</b> {action_id}",);
+ if let Some(subject) = details.remove("polkit.caller-pid") {
+ let _ = write!(description, "\n<b>Caller:</b> ");
+ if let Ok(pid) = subject.parse::<u32>() {
+ let _ = write!(description, "{}", PidDisplay(pid));
+ } else {
+ let _ = write!(description, "{}", emphasize("invalid pid"));
+ }
+ }
+ if let Some(subject) = details.remove("polkit.subject-pid") {
+ let _ = write!(description, "\n<b>Subject:</b> ");
+ if let Ok(pid) = subject.parse::<u32>() {
+ let _ = write!(description, "{}", PidDisplay(pid));
+ } else {
+ let _ = write!(description, "{}", emphasize("invalid pid"));
+ }
+ }
+ let mut prompter = PrependSourcePrompter {
+ source: vec![Source(Cow::Borrowed("polkit agent"))],
+ description: description.clone(),
+ prompter: RofiPrompter,
+ };
+
+ let identity_displays: Vec<String> =
+ identities.iter().map(|v| v.to_string()).collect();
+ let identity_displays: Vec<&str> =
+ identity_displays.iter().map(|v| v.as_str()).collect();
+ let choosen_identity = match identity_displays.len() {
+ 0 => {
+ return Err(fdo::Error::AuthFailed(
+ "no identity to authenticate as".to_owned(),
+ ))
+ }
+ 1 => 0,
+ _ => {
+ prompter
+ .prompt_enum(
+ "Identity",
+ "Select identity to use for polkit authorization",
+ &identity_displays,
+ &[],
+ )
+ .await?
+ }
+ };
+
+ let _ = write!(
+ description,
+ "\n<b>Identity:</b> {}",
+ identities[choosen_identity as usize]
+ );
+ prompter.description = description;
+
+ prompter.source.push(Source(Cow::Borrowed("polkit daemon")));
+ let prompter = TemporaryPrompterInterface::new(connection, prompter).await;
helper
.init_conversation(
BackendRequest {
@@ -100,7 +156,7 @@
environment: HashMap::new(),
prompter_path: prompter.path.clone(),
// TODO: Let user choose
- identity: identities.get(0).expect("first always exists").clone(),
+ identity: identities[choosen_identity as usize].clone(),
}, // cookie.to_owned(), HashMap::new(), prompter.path.clone()
)
.await?;
@@ -135,7 +191,7 @@
}
}
-const OBJ_PATH: &str = "/0lach/polkitAgent";
+const OBJ_PATH: &str = "/org/freedesktop/PolicyKit1/AuthenticationAgent";
#[proxy(
interface = "lach.PolkitHelper",
crates/polkit-shared/Cargo.tomldiffbeforeafterboth--- a/crates/polkit-shared/Cargo.toml
+++ b/crates/polkit-shared/Cargo.toml
@@ -4,5 +4,6 @@
edition = "2021"
[dependencies]
+nix = "0.29.0"
serde = { version = "1.0.204", features = ["derive"] }
zbus = "4.4.0"
crates/polkit-shared/src/lib.rsdiffbeforeafterboth--- a/crates/polkit-shared/src/lib.rs
+++ b/crates/polkit-shared/src/lib.rs
@@ -1,14 +1,83 @@
use std::collections::HashMap;
+use std::{fmt, fs};
+use nix::unistd::{Uid, User};
use serde::{Deserialize, Serialize};
-use zbus::zvariant::{OwnedValue, Type};
+use zbus::zvariant::{OwnedValue, Type, Value};
+
+pub fn emphasize(s: impl AsRef<str>) -> String {
+ format!("<span style=\"italic\"><{}></span>", escape(s),)
+}
+fn command(s: impl AsRef<str>) -> String {
+ format!("<u><tt>{}</tt></u>", s.as_ref())
+}
+fn escape(s: impl AsRef<str>) -> String {
+ s.as_ref()
+ .replace("&", """)
+ .replace("<", "<")
+ .replace(">", ">")
+}
+pub struct PidDisplay(pub u32);
+impl fmt::Display for PidDisplay {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.0 == 1 {
+ emphasize("init").fmt(f)
+ } else if let Ok(proc) = fs::read_to_string(format!("/proc/{}/cmdline", self.0)) {
+ write!(
+ f,
+ "<sub>command</sub>{}",
+ command(
+ proc.replace("\0", " ")
+ .strip_suffix(" ")
+ .expect("cmdline should end with NUL")
+ )
+ )
+ } else if let Ok(proc) = fs::read_to_string(format!("/proc/{}/comm", self.0)) {
+ write!(f, "<sub>process</sub>{}", command(proc.replace("\0", " ")))
+ } else {
+ emphasize("unknown process").fmt(f)
+ }
+ }
+}
+
#[derive(Serialize, Deserialize, Type, PartialEq, Debug)]
pub struct Identity {
pub kind: String,
pub details: HashMap<String, OwnedValue>,
}
+impl fmt::Display for Identity {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.kind.as_str() {
+ "unix-user" => match self.details.get("uid").map(|v| &**v) {
+ Some(Value::U32(uid)) => match User::from_uid(Uid::from_raw(*uid)) {
+ Ok(Some(u)) => write!(
+ f,
+ "<sub>user</sub>{}<sup>{}</sup>{}",
+ u.name,
+ u.uid,
+ if u.gecos.is_empty() {
+ "".to_owned()
+ } else {
+ format!(": {}", escape(u.gecos.to_string_lossy()))
+ }
+ ),
+ Ok(None) => emphasize("not found").fmt(f),
+ Err(e) => {
+ let user = format!("could not get user: {e}");
+ emphasize(&user).fmt(f)?;
+ Ok(())
+ }
+ },
+
+ _ => emphasize("unknown uid").fmt(f),
+ },
+ _ => emphasize(format!("identity of unknown kind: {}", self.kind)).fmt(f),
+ }
+ }
+}
+
impl Clone for Identity {
fn clone(&self) -> Self {
Self {
crates/ui-prompt/src/lib.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/lib.rs
+++ b/crates/ui-prompt/src/lib.rs
@@ -18,7 +18,7 @@
#[cfg_attr(feature = "dbus", derive(zbus::zvariant::Type))]
#[derive(serde::Serialize, serde::Deserialize, Clone)]
-pub struct Source(Cow<'static, str>);
+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)
@@ -79,8 +79,9 @@
}
pub struct PrependSourcePrompter<P> {
- prompter: P,
- source: Vec<Source>,
+ pub prompter: P,
+ pub source: Vec<Source>,
+ pub description: String,
}
impl<P> PrependSourcePrompter<P> {
fn source(&self, input: &[Source]) -> Vec<Source> {
@@ -88,11 +89,31 @@
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,
@@ -101,7 +122,12 @@
source: &[Source],
) -> Result<u32> {
self.prompter
- .prompt_enum(prompt, description, variants, &self.source(source))
+ .prompt_enum(
+ prompt,
+ dbg!(&self.description(description)),
+ variants,
+ &self.source(source),
+ )
.await
}
@@ -113,13 +139,18 @@
source: &[Source],
) -> Result<String> {
self.prompter
- .prompt_text(echo, prompt, description, &self.source(source))
+ .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, description, &self.source(source))
+ .display_text(error, &self.description(description), &self.source(source))
.await
}
}
crates/ui-prompt/src/rofi.rsdiffbeforeafterboth--- a/crates/ui-prompt/src/rofi.rs
+++ b/crates/ui-prompt/src/rofi.rs
@@ -27,7 +27,10 @@
description.to_owned()
} else {
let mut out = format!("{description}\n\n<b>Requested on ",);
- for s in source.iter() {
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
out.push_str(&s.to_string());
}
out.push_str("</b>");
@@ -43,6 +46,7 @@
fixup_prompt(prompt),
"-format",
"i",
+ "-markup-rows",
]);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
@@ -100,7 +104,10 @@
description.to_owned()
} else {
let mut out = format!("{description}\n\n<b>Requested on ",);
- for s in source.iter() {
+ for (i, s) in source.iter().enumerate() {
+ if i != 0 {
+ out.push_str(" -> ");
+ }
out.push_str(&s.to_string());
}
out.push_str("</b>");