git.delta.rocks / remowt / refs/commits / 6bfa3e083e52

difftreelog

feat(ui-prompt) declarative bifrost endpoint

nswuwzxtYaroslav Bolyukin2026-01-25parent: #06498c3.patch.diff
in: trunk

5 files changed

modifiedcrates/ui-prompt/Cargo.tomldiffbeforeafterboth
55
6[dependencies]6[dependencies]
7bifrostlink.workspace = true7bifrostlink.workspace = true
8bifrostlink-macros.workspace = true
8serde = "1.0.204"9serde.workspace = true
10serde_json.workspace = true
9thiserror = "1.0.63"11thiserror = "1.0.63"
10tokio = { version = "1.39.2", features = ["io-util", "macros", "process", "rt"] }12tokio = { workspace = true, features = ["io-util", "macros", "process", "rt"] }
11tracing = "0.1.40"13tracing.workspace = true
12zbus = { version = "4.4.0", optional = true }14zbus = { workspace = true, optional = true }
1315
14[features]16[features]
15default = ["dbus"]17default = ["dbus"]
modifiedcrates/ui-prompt/src/bifrost.rsdiffbeforeafterboth
1use bifrostlink::error::ErrorT;1use bifrostlink::{Config, Rpc};
2use bifrostlink::{request, AddressT, Rpc};2use bifrostlink_macros::endpoints;
3use serde::{Deserialize, Serialize};3use serde::{Deserialize, Serialize};
44
5use crate::{Error, Prompter, Source};5use crate::{Error, Prompter, Source};
66
7pub struct BifrostPrompter<A: AddressT, E: ErrorT> {7pub struct PromptEndpoints<P>(pub P);
8 pub address: A,8
9#[endpoints(ns = 2)]
10impl<P> PromptEndpoints<P>
11where
9 pub rpc: Rpc<A, E>,12 P: Prompter + Send + Sync + 'static,
10}13{
11
12#[derive(Serialize, Deserialize)]14 #[endpoints(id = 1, cancel)]
13struct EnumRequest {15 async fn prompt_enum(
16 &self,
14 prompt: String,17 prompt: String,
15 description: String,18 description: String,
16 variants: Vec<String>,19 variants: Vec<String>,
17 source: Vec<Source>,20 source: Vec<Source>,
18}21 ) -> Result<u32, Error> {
19#[derive(Serialize, Deserialize)]22 let variants: Vec<&str> = variants.iter().map(|v| v.as_str()).collect();
20struct EnumResponse {23 self.0
21 value: u32,24 .prompt_enum(&prompt, &description, &variants, &source)
22}25 .await
23request!(EnumRequest => EnumResponse);26 }
2427
28 #[endpoints(id = 2, cancel)]
29 async fn prompt_text(
25#[derive(Serialize, Deserialize)]30 &self,
26struct TextRequest {
27 echo: bool,31 echo: bool,
28 prompt: String,32 prompt: String,
29 description: String,33 description: String,
30 source: Vec<Source>,34 source: Vec<Source>,
31}
32#[derive(Serialize, Deserialize)]
33struct TextResponse {
34 value: String,35 ) -> Result<String, Error> {
35}36 self.0
36request!(TextRequest => TextResponse);37 .prompt_text(echo, &prompt, &description, &source)
38 .await
39 }
3740
38#[derive(Serialize, Deserialize)]41 #[endpoints(id = 3, cancel)]
39struct DisplayRequest {42 async fn display_text(
43 &self,
40 error: bool,44 error: bool,
41 description: String,45 description: String,
42 source: Vec<Source>,46 source: Vec<Source>,
43}47 ) -> Result<(), Error> {
44request!(DisplayRequest => ());48 self.0.display_text(error, &description, &source).await
49 }
50}
4551
46impl<A: AddressT, E: ErrorT> Prompter for BifrostPrompter<A, E>52impl<C: Config> Prompter for PromptEndpointsClient<C>
47where53where
48 crate::Error: From<E>,54 Error: ToString,
49{55{
50 async fn prompt_enum(56 async fn prompt_enum(
51 &self,57 &self,
52 prompt: &str,58 prompt: &str,
53 description: &str,59 description: &str,
54 variants: &[&str],60 variants: &[&str],
55 source: &[crate::Source],61 source: &[Source],
56 ) -> crate::Result<u32> {62 ) -> crate::Result<u32> {
57 let res = self
58 .rpc
59 .request(
60 self.address.clone(),63 self.prompt_enum(
61 &EnumRequest {
62 prompt: prompt.to_owned(),64 prompt.to_owned(),
63 description: description.to_owned(),65 description.to_owned(),
64 variants: variants.into_iter().map(|v| (*v).to_owned()).collect(),66 variants.iter().map(|v| (*v).to_owned()).collect(),
65 source: source.to_vec(),67 source.to_vec(),
66 },68 )
67 )69 .await
68 .await?;70 .map_err(|e| Error::Remote(e.to_string()))?
69 Ok(res.value)
70 }71 }
7172
72 async fn prompt_text(73 async fn prompt_text(
73 &self,74 &self,
74 echo: bool,75 echo: bool,
75 prompt: &str,76 prompt: &str,
76 description: &str,77 description: &str,
77 source: &[crate::Source],78 source: &[Source],
78 ) -> crate::Result<String> {79 ) -> crate::Result<String> {
79 let res = self
80 .rpc
81 .request(
82 self.address.clone(),80 self.prompt_text(
83 &TextRequest {
84 echo,81 echo,
85 prompt: prompt.to_owned(),82 prompt.to_owned(),
86 description: description.to_owned(),83 description.to_owned(),
87 source: source.to_vec(),84 source.to_vec(),
88 },85 )
89 )86 .await
90 .await?;87 .map_err(|e| Error::Remote(e.to_string()))?
91 Ok(res.value)
92 }88 }
9389
94 async fn display_text(90 async fn display_text(
95 &self,91 &self,
96 error: bool,92 error: bool,
97 description: &str,93 description: &str,
98 source: &[crate::Source],94 source: &[Source],
99 ) -> crate::Result<()> {95 ) -> crate::Result<()> {
100 self.rpc
101 .request(
102 self.address.clone(),96 self.display_text(error, description.to_owned(), source.to_vec())
103 &DisplayRequest {97 .await
104 error,
105 description: description.to_owned(),
106 source: source.to_vec(),
107 },
108 )
109 .await?;98 .map_err(|e| Error::Remote(e.to_string()))?
110 Ok(())
111 }99 }
112}100}
113101
114pub fn handle_bifrost_prompts<102pub fn serve_prompts<P, C>(rpc: &mut Rpc<C>, prompt: P)
115 P: Prompter + Clone + 'static,103where
116 A: AddressT,
117 E: ErrorT + From<Error>,
118>(
119 rpc: &Rpc<A, E>,
120 prompt: P,
121) {
122 rpc.register_request_handler(true, {
123 let prompt = prompt.clone();
124 move |_addr, req: EnumRequest| {104 P: Prompter + Send + Sync + 'static,
125 let prompt = prompt.clone();
126 async move {
127 let i = prompt
128 .prompt_enum(
129 &req.prompt,105 C: Config,
130 &req.description,
131 &req.variants.iter().map(|v| v.as_str()).collect::<Vec<_>>(),106 C::Error: From<Error>,
132 &req.source,
133 )
134 .await?;
135
136 Ok(EnumResponse { value: i })
137 }
138 }
139 });
140 rpc.register_request_handler(true, {107{
141 let prompt = prompt.clone();
142 move |_addr, req: TextRequest| {
143 let prompt = prompt.clone();
144 async move {
145 let i = prompt
146 .prompt_text(req.echo, &req.prompt, &req.description, &req.source)108 PromptEndpoints(prompt).register_endpoints(rpc);
147 .await?;
148
149 Ok(TextResponse { value: i })
150 }
151 }
152 });109}
153 rpc.register_request_handler(true, {
154 let prompt = prompt.clone();
155 move |_addr, req: DisplayRequest| {
156 let prompt = prompt.clone();
157 async move {
158 prompt
159 .display_text(req.error, &req.description, &req.source)
160 .await?;
161
162 Ok(())
163 }
164 }
165 });
166}
167110
modifiedcrates/ui-prompt/src/dbus.rsdiffbeforeafterboth
39}40}
4041
41#[proxy(interface = "lach.PolkitInputHandler")]42#[proxy(interface = "lach.PolkitInputHandler")]
42trait DbusPrompter {43pub trait DbusPrompter {
43 async fn prompt_enum(44 async fn prompt_enum(
44 &self,45 &self,
45 prompt: &str,46 prompt: &str,
127 fn from(value: Error) -> Self {128 fn from(value: Error) -> Self {
128 match value {129 match value {
129 Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),130 Error::Cancel => fdo::Error::NoReply("input was cancelled".to_owned()),
131 Error::Remote(e) => fdo::Error::NoReply(format!("remote error occured: {e}")),
130 Error::InputError(e) => fdo::Error::Failed(e),132 Error::InputError(e) => fdo::Error::Failed(e),
131 }133 }
132 }134 }
modifiedcrates/ui-prompt/src/lib.rsdiffbeforeafterboth
3use std::future::Future;3use std::future::Future;
4use std::result;4use std::result;
55
6pub mod dbus;6pub mod bifrost;
7pub mod rofi;7pub mod dbus;
8pub mod bifrost;8pub mod rofi;
99
10#[derive(thiserror::Error, Debug)]10#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
11pub enum Error {11pub enum Error {
12 #[error("user has cancelled input")]12 #[error("user has cancelled input")]
13 Cancel,13 Cancel,
14 #[error("input error: {0}")]14 #[error("input error: {0}")]
15 InputError(String),15 InputError(String),
16 #[error("unknown remote error: {0}")]
17 Remote(String),
16}18}
1719
18pub type Result<T, E = Error> = result::Result<T, E>;20pub type Result<T, E = Error> = result::Result<T, E>;
167 self.prompter169 self.prompter
168 .prompt_enum(170 .prompt_enum(
169 prompt,171 prompt,
170 dbg!(&self.description(description)),172 &self.description(description),
171 variants,173 variants,
172 &self.source(source),174 &self.source(source),
173 )175 )
modifiedcrates/ui-prompt/src/rofi.rsdiffbeforeafterboth
66
7use crate::{Error, Prompter, Result, Source};7use crate::{Error, Prompter, Result, Source};
88
9#[derive(Clone)]
9pub struct RofiPrompter;10pub struct RofiPrompter;
1011
11fn fixup_prompt(prompt: &str) -> &str {12fn fixup_prompt(prompt: &str) -> &str {
12 // Rofi always appends such suffix13 // Rofi always appends such suffix
13 prompt.strip_suffix(": ").unwrap_or(prompt)14 prompt.strip_suffix(": ").unwrap_or(prompt)
14}15}
16
17fn rofi_command() -> Command {
18 Command::new(option_env!("ROFI").unwrap_or("rofi"))
19}
1520
16impl Prompter for RofiPrompter {21impl Prompter for RofiPrompter {
17 async fn prompt_enum(22 async fn prompt_enum(
22 source: &[Source],27 source: &[Source],
23 ) -> Result<u32> {28 ) -> Result<u32> {
24 trace!("rofi radio");29 trace!("rofi radio");
25 let mut cmd = Command::new("rofi");30 let mut cmd = rofi_command();
26 let mesg = if source.is_empty() {31 let mesg = if source.is_empty() {
27 description.to_owned()32 description.to_owned()
28 } else {33 } else {
99 source: &[Source],104 source: &[Source],
100 ) -> Result<String> {105 ) -> Result<String> {
101 trace!("rofi text");106 trace!("rofi text");
102 let mut cmd = Command::new("rofi");107 let mut cmd = rofi_command();
103 let mesg = if source.is_empty() {108 let mesg = if source.is_empty() {
104 description.to_owned()109 description.to_owned()
105 } else {110 } else {
139144
140 async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {145 async fn display_text(&self, error: bool, description: &str, source: &[Source]) -> Result<()> {
141 trace!("rofi display");146 trace!("rofi display");
142 let mut cmd = Command::new("rofi");147 let mut cmd = rofi_command();
143 let mut mesg = if source.is_empty() {148 let mut mesg = if source.is_empty() {
144 description.to_owned()149 description.to_owned()
145 } else {150 } else {
178 use crate::rofi::RofiPrompter;183 use crate::rofi::RofiPrompter;
179 use crate::{PrependSourcePrompter, Prompter as _, Source};184 use crate::{PrependSourcePrompter, Prompter as _, Source};
180185
186 // #[tokio::test]
181 #[tokio::test]187 #[tokio::test]
188 #[ignore = "interactive"]
182 async fn test() {189 async fn test() {
183 let prompter = PrependSourcePrompter {190 let prompter = PrependSourcePrompter {
184 prompter: RofiPrompter,191 prompter: RofiPrompter,
192 description: "test".to_owned(),
185 source: vec![Source(Cow::Borrowed("ssh"))],193 source: vec![Source(Cow::Borrowed("ssh"))],
186 };194 };
187 prompter195 prompter