summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/post_editor.rs216
-rw-r--r--src/components/preferences.rs72
-rw-r--r--src/components/signin.rs251
-rw-r--r--src/components/smart_summary.rs158
-rw-r--r--src/components/tag_pill.rs4
-rw-r--r--src/lib.rs406
-rw-r--r--src/main.rs12
-rw-r--r--src/micropub.rs50
-rw-r--r--src/secrets.rs6
-rw-r--r--src/util.rs8
10 files changed, 715 insertions, 468 deletions
diff --git a/src/components/post_editor.rs b/src/components/post_editor.rs
index d08685a..021ba91 100644
--- a/src/components/post_editor.rs
+++ b/src/components/post_editor.rs
@@ -1,11 +1,16 @@
-use gettextrs::*;
 use crate::components::tag_pill::*;
 use adw::prelude::*;
+use gettextrs::*;
 
 use glib::translate::IntoGlib;
-use relm4::{factory::FactoryVecDeque, gtk, prelude::{Controller, DynamicIndex}, Component, ComponentParts, ComponentSender, RelmWidgetExt};
 #[cfg(feature = "smart-summary")]
 use relm4::prelude::ComponentController;
+use relm4::{
+    factory::FactoryVecDeque,
+    gtk,
+    prelude::{Controller, DynamicIndex},
+    Component, ComponentParts, ComponentSender, RelmWidgetExt,
+};
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
 #[enum_type(name = "MicropubVisibility")]
@@ -18,7 +23,7 @@ impl std::fmt::Display for Visibility {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.write_str(match self {
             Self::Public => "public",
-            Self::Private => "private"
+            Self::Private => "private",
         })
     }
 }
@@ -29,7 +34,7 @@ pub struct Post {
     pub summary: Option<String>,
     pub tags: Vec<String>,
     pub content: String,
-    pub visibility: Visibility
+    pub visibility: Visibility,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -39,39 +44,36 @@ pub struct PostConversionSettings {
 
 impl Post {
     pub fn into_mf2(self, settings: PostConversionSettings) -> microformats::types::Item {
-        use microformats::types::{Item, Class, KnownClass, PropertyValue, Fragment};
+        use microformats::types::{Class, Fragment, Item, KnownClass, PropertyValue};
         let mut mf2 = Item::new(vec![Class::Known(KnownClass::Entry)]);
 
         if let Some(name) = self.name {
-            mf2.properties.insert(
-                "name".to_owned(), vec![PropertyValue::Plain(name)]
-            );
+            mf2.properties
+                .insert("name".to_owned(), vec![PropertyValue::Plain(name)]);
         }
 
         if let Some(summary) = self.summary {
-            mf2.properties.insert(
-                "summary".to_owned(),
-                vec![PropertyValue::Plain(summary)]
-            );
+            mf2.properties
+                .insert("summary".to_owned(), vec![PropertyValue::Plain(summary)]);
         }
 
         if !self.tags.is_empty() {
             mf2.properties.insert(
                 "category".to_string(),
-                self.tags.into_iter().map(PropertyValue::Plain).collect()
+                self.tags.into_iter().map(PropertyValue::Plain).collect(),
             );
         }
 
         mf2.properties.insert(
             "visibility".to_string(),
-            vec![PropertyValue::Plain(self.visibility.to_string())]
+            vec![PropertyValue::Plain(self.visibility.to_string())],
         );
 
         let content = if settings.send_html_directly {
             PropertyValue::Fragment(Fragment {
                 html: self.content.clone(),
                 value: self.content,
-                lang: None
+                lang: None,
             })
         } else {
             PropertyValue::Plain(self.content)
@@ -86,24 +88,35 @@ impl Post {
 #[tracker::track]
 #[derive(Debug)]
 pub(crate) struct PostEditor<E> {
-    #[no_eq] smart_summary_busy_guard: Option<gtk::gio::ApplicationBusyGuard>,
+    #[no_eq]
+    smart_summary_busy_guard: Option<gtk::gio::ApplicationBusyGuard>,
     sending: bool,
-    #[do_not_track] #[allow(dead_code)] spell_checker: spelling::Checker,
-    #[do_not_track] spelling_adapter: spelling::TextBufferAdapter,
-
-    #[do_not_track] name_buffer: gtk::EntryBuffer,
-    #[do_not_track] summary_buffer: gtk::EntryBuffer,
-    #[do_not_track] content_buffer: sourceview5::Buffer,
-
-    #[do_not_track] pending_tag_buffer: gtk::EntryBuffer,
-    #[do_not_track] tags: relm4::factory::FactoryVecDeque<TagPill>,
+    #[do_not_track]
+    #[allow(dead_code)]
+    spell_checker: spelling::Checker,
+    #[do_not_track]
+    spelling_adapter: spelling::TextBufferAdapter,
+
+    #[do_not_track]
+    name_buffer: gtk::EntryBuffer,
+    #[do_not_track]
+    summary_buffer: gtk::EntryBuffer,
+    #[do_not_track]
+    content_buffer: sourceview5::Buffer,
+
+    #[do_not_track]
+    pending_tag_buffer: gtk::EntryBuffer,
+    #[do_not_track]
+    tags: relm4::factory::FactoryVecDeque<TagPill>,
     visibility: Visibility,
 
-    #[do_not_track] narrow_layout: gtk::BoxLayout,
+    #[do_not_track]
+    narrow_layout: gtk::BoxLayout,
 
     #[cfg(feature = "smart-summary")]
-    #[do_not_track] smart_summary: Controller<crate::components::SmartSummaryButton>,
-    _err: std::marker::PhantomData<E>
+    #[do_not_track]
+    smart_summary: Controller<crate::components::SmartSummaryButton>,
+    _err: std::marker::PhantomData<E>,
 }
 
 impl<E> PostEditor<E> {
@@ -120,13 +133,17 @@ impl<E> PostEditor<E> {
 #[allow(clippy::manual_non_exhaustive)] // false positive
 pub enum Input<E: std::error::Error + std::fmt::Debug + Send + 'static> {
     #[cfg(feature = "smart-summary")]
-    #[doc(hidden)] SmartSummary(crate::components::smart_summary::Output),
-    #[doc(hidden)] VisibilitySelected(Visibility),
-    #[doc(hidden)] AddTagFromBuffer,
-    #[doc(hidden)] RemoveTag(DynamicIndex),
+    #[doc(hidden)]
+    SmartSummary(crate::components::smart_summary::Output),
+    #[doc(hidden)]
+    VisibilitySelected(Visibility),
+    #[doc(hidden)]
+    AddTagFromBuffer,
+    #[doc(hidden)]
+    RemoveTag(DynamicIndex),
     Submit,
     SubmitDone(glib::Uri),
-    SubmitError(E)
+    SubmitError(E),
 }
 
 #[relm4::component(pub)]
@@ -340,7 +357,7 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
     fn init(
         init: Self::Init,
         root: Self::Root,
-        sender: ComponentSender<Self>
+        sender: ComponentSender<Self>,
     ) -> ComponentParts<Self> {
         #[cfg(feature = "smart-summary")]
         let (http, init) = init;
@@ -352,17 +369,13 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
             smart_summary_busy_guard: None,
             sending: false,
 
-            spelling_adapter: spelling::TextBufferAdapter::new(
-                &content_buffer,
-                &spell_checker
-            ),
+            spelling_adapter: spelling::TextBufferAdapter::new(&content_buffer, &spell_checker),
             spell_checker,
 
             name_buffer: gtk::EntryBuffer::default(),
             summary_buffer: gtk::EntryBuffer::default(),
             content_buffer,
 
-
             pending_tag_buffer: gtk::EntryBuffer::default(),
             tags: FactoryVecDeque::builder()
                 .launch({
@@ -375,10 +388,9 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
                     listbox.set_layout_manager(Some(layout));
                     listbox
                 })
-                .forward(
-                    sender.input_sender(),
-                    |del: TagPillDelete| Input::RemoveTag(del.0)
-                ),
+                .forward(sender.input_sender(), |del: TagPillDelete| {
+                    Input::RemoveTag(del.0)
+                }),
             visibility: Visibility::Public,
 
             narrow_layout: gtk::BoxLayout::builder()
@@ -403,15 +415,15 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
 
         model.spelling_adapter.set_enabled(true);
 
-        widgets.visibility_selector.set_expression(Some(
-            gtk::ClosureExpression::new::<String>(
+        widgets
+            .visibility_selector
+            .set_expression(Some(gtk::ClosureExpression::new::<String>(
                 [] as [gtk::Expression; 0],
                 glib::closure::RustClosure::new(|v| {
                     let list_item = v[0].get::<adw::EnumListItem>().unwrap();
                     Some(gettext(list_item.name().as_str()).into())
-                })
-            )
-        ));
+                }),
+            )));
 
         if let Some(post) = init {
             if let Some(name) = post.name {
@@ -422,55 +434,68 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
             }
 
             let mut tags = model.tags.guard();
-            post.tags.into_iter().for_each(|t| { tags.push_back(t.into_boxed_str()); });
+            post.tags.into_iter().for_each(|t| {
+                tags.push_back(t.into_boxed_str());
+            });
 
             model.content_buffer.set_text(&post.content);
 
-            widgets.visibility_selector.set_selected(
-                visibility_model.find_position(post.visibility.into_glib())
-            );
+            widgets
+                .visibility_selector
+                .set_selected(visibility_model.find_position(post.visibility.into_glib()));
             model.visibility = post.visibility;
         }
 
         ComponentParts { model, widgets }
     }
 
-    fn update_with_view(&mut self, widgets: &mut Self::Widgets, msg: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
+    fn update_with_view(
+        &mut self,
+        widgets: &mut Self::Widgets,
+        msg: Self::Input,
+        sender: ComponentSender<Self>,
+        root: &Self::Root,
+    ) {
         self.reset();
         match msg {
             #[cfg(feature = "smart-summary")]
             Input::SmartSummary(crate::components::SmartSummaryOutput::Start) => {
                 widgets.content_textarea.set_sensitive(false);
                 if self.content_buffer.char_count() == 0 {
-                    let _ = self.smart_summary.sender().send(
-                        crate::components::SmartSummaryInput::Cancel
-                    );
+                    let _ = self
+                        .smart_summary
+                        .sender()
+                        .send(crate::components::SmartSummaryInput::Cancel);
                 } else {
                     let text = self.content_buffer.text(
                         &self.content_buffer.start_iter(),
                         &self.content_buffer.end_iter(),
-                        false
+                        false,
                     );
 
-                    self.set_smart_summary_busy_guard(
-                        Some(relm4::main_adw_application().mark_busy())
-                    );
-                    if self.smart_summary.sender().send(
-                        crate::components::SmartSummaryInput::Text(text.into())
-                    ).is_ok() {
+                    self.set_smart_summary_busy_guard(Some(
+                        relm4::main_adw_application().mark_busy(),
+                    ));
+                    if self
+                        .smart_summary
+                        .sender()
+                        .send(crate::components::SmartSummaryInput::Text(text.into()))
+                        .is_ok()
+                    {
                         self.summary_buffer.set_text("");
                     }
                 }
                 widgets.content_textarea.set_sensitive(true);
-            },
+            }
             #[cfg(feature = "smart-summary")]
             Input::SmartSummary(crate::components::SmartSummaryOutput::Chunk(text)) => {
-                self.summary_buffer.insert_text(self.summary_buffer.length(), text);
-            },
+                self.summary_buffer
+                    .insert_text(self.summary_buffer.length(), text);
+            }
             #[cfg(feature = "smart-summary")]
             Input::SmartSummary(crate::components::SmartSummaryOutput::Done) => {
                 self.set_smart_summary_busy_guard(None);
-            },
+            }
             #[cfg(feature = "smart-summary")]
             Input::SmartSummary(crate::components::SmartSummaryOutput::Error(err)) => {
                 self.set_smart_summary_busy_guard(None);
@@ -479,44 +504,51 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
                 toast.set_timeout(0);
                 toast.set_priority(adw::ToastPriority::High);
                 root.add_toast(toast);
-            },
+            }
             Input::VisibilitySelected(vis) => {
                 log::debug!("Changed visibility: {}", vis);
                 self.visibility = vis;
-            },
+            }
             Input::AddTagFromBuffer => {
                 let tag = String::from(self.pending_tag_buffer.text());
                 if !tag.is_empty() {
-                    self.tags.guard().push_back(
-                        tag.into_boxed_str()
-                    );
+                    self.tags.guard().push_back(tag.into_boxed_str());
                     self.pending_tag_buffer.set_text("");
                 }
-            },
+            }
             Input::RemoveTag(idx) => {
                 self.tags.guard().remove(idx.current_index());
-            },
+            }
             Input::Submit => {
                 self.sending = true;
                 let post = if self.content_buffer.char_count() > 0 {
                     Some(Post {
                         name: if self.name_buffer.length() > 0 {
                             Some(self.name_buffer.text().into())
-                        } else { None },
+                        } else {
+                            None
+                        },
                         summary: if self.summary_buffer.length() > 0 {
                             Some(self.summary_buffer.text().into())
-                        } else { None },
+                        } else {
+                            None
+                        },
                         tags: self.tags.iter().map(|t| t.0.clone().into()).collect(),
-                        content: self.content_buffer.text(
-                            &self.content_buffer.start_iter(),
-                            &self.content_buffer.end_iter(),
-                            false
-                        ).into(),
+                        content: self
+                            .content_buffer
+                            .text(
+                                &self.content_buffer.start_iter(),
+                                &self.content_buffer.end_iter(),
+                                false,
+                            )
+                            .into(),
                         visibility: self.visibility,
                     })
-                } else { None };
+                } else {
+                    None
+                };
                 let _ = sender.output(post);
-            },
+            }
             Input::SubmitDone(location) => {
                 self.name_buffer.set_text("");
                 self.summary_buffer.set_text("");
@@ -528,18 +560,22 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
                     gtk::UriLauncher::new(&location.to_string()).launch(
                         None::<&adw::ApplicationWindow>,
                         None::<&gio::Cancellable>,
-                        glib::clone!(#[weak] toast, move |result| {
-                            if let Err(err) = result {
-                                log::warn!("Error opening post URI: {}", err);
-                            } else {
-                                toast.dismiss()
+                        glib::clone!(
+                            #[weak]
+                            toast,
+                            move |result| {
+                                if let Err(err) = result {
+                                    log::warn!("Error opening post URI: {}", err);
+                                } else {
+                                    toast.dismiss()
+                                }
                             }
-                        })
+                        ),
                     );
                 });
 
                 root.add_toast(toast);
-            },
+            }
             Input::SubmitError(err) => {
                 let toast = adw::Toast::new(&gettext!("Error sending post: {}", err));
                 toast.set_timeout(0);
diff --git a/src/components/preferences.rs b/src/components/preferences.rs
index 67075a2..2b3e640 100644
--- a/src/components/preferences.rs
+++ b/src/components/preferences.rs
@@ -1,7 +1,7 @@
 use gettextrs::*;
 
-use gio::prelude::*;
 use adw::prelude::*;
+use gio::prelude::*;
 use relm4::prelude::*;
 
 pub struct Preferences {
@@ -19,7 +19,9 @@ impl ComposerPreferencesWidgets {
     fn new(settings: &gio::Settings) -> Self {
         let page = adw::PreferencesPage::builder()
             .title(gettext("Post composer"))
-            .description(gettext("Settings for composing new posts and editing existing ones."))
+            .description(gettext(
+                "Settings for composing new posts and editing existing ones.",
+            ))
             .icon_name("editor-symbolic")
             .build();
         let general_group = adw::PreferencesGroup::builder()
@@ -32,20 +34,21 @@ impl ComposerPreferencesWidgets {
         let widgets = Self {
             page,
             general_group,
-            send_html_directly
+            send_html_directly,
         };
 
         let schema = settings.settings_schema().unwrap();
 
         #[expect(clippy::single_element_loop)]
-        for (row, key, property) in [
-            (widgets.send_html_directly.upcast_ref::<adw::PreferencesRow>(), "send-html-directly", "active"),
-        ] {
+        for (row, key, property) in [(
+            widgets
+                .send_html_directly
+                .upcast_ref::<adw::PreferencesRow>(),
+            "send-html-directly",
+            "active",
+        )] {
             let key_data = schema.key(key);
-            settings.bind(key, row, property)
-                .get()
-                .set()
-                .build();
+            settings.bind(key, row, property).get().set().build();
             row.set_title(&gettext(key_data.summary().unwrap()));
             row.set_tooltip_markup(key_data.description().map(gettext).as_deref());
         }
@@ -62,7 +65,7 @@ struct LanguageModelPreferencesWidgets {
     general_group: adw::PreferencesGroup,
     llm_endpoint: adw::EntryRow,
     smart_summary_show_warning: adw::SwitchRow,
-    
+
     smart_summary_group: adw::PreferencesGroup,
     smart_summary_model: adw::EntryRow,
     smart_summary_system_prompt: adw::EntryRow,
@@ -112,24 +115,45 @@ impl LanguageModelPreferencesWidgets {
             smart_summary_model,
             smart_summary_system_prompt,
             smart_summary_prompt_prefix,
-            smart_summary_prompt_suffix
+            smart_summary_prompt_suffix,
         };
 
         let schema = settings.settings_schema().unwrap();
 
         for (row, key, property) in [
-            (widgets.llm_endpoint.upcast_ref::<adw::PreferencesRow>(), "llm-endpoint", "text"),
-            (widgets.smart_summary_show_warning.upcast_ref::<_>(), "smart-summary-show-warning", "active"),
-            (widgets.smart_summary_model.upcast_ref::<_>(), "smart-summary-model", "text"),
-            (widgets.smart_summary_system_prompt.upcast_ref::<_>(), "smart-summary-system-prompt", "text"),
-            (widgets.smart_summary_prompt_prefix.upcast_ref::<_>(), "smart-summary-prompt-prefix", "text"),
-            (widgets.smart_summary_prompt_suffix.upcast_ref::<_>(), "smart-summary-prompt-suffix", "text"),
+            (
+                widgets.llm_endpoint.upcast_ref::<adw::PreferencesRow>(),
+                "llm-endpoint",
+                "text",
+            ),
+            (
+                widgets.smart_summary_show_warning.upcast_ref::<_>(),
+                "smart-summary-show-warning",
+                "active",
+            ),
+            (
+                widgets.smart_summary_model.upcast_ref::<_>(),
+                "smart-summary-model",
+                "text",
+            ),
+            (
+                widgets.smart_summary_system_prompt.upcast_ref::<_>(),
+                "smart-summary-system-prompt",
+                "text",
+            ),
+            (
+                widgets.smart_summary_prompt_prefix.upcast_ref::<_>(),
+                "smart-summary-prompt-prefix",
+                "text",
+            ),
+            (
+                widgets.smart_summary_prompt_suffix.upcast_ref::<_>(),
+                "smart-summary-prompt-suffix",
+                "text",
+            ),
         ] {
             let key_data = schema.key(key);
-            settings.bind(key, row, property)
-                .get()
-                .set()
-                .build();
+            settings.bind(key, row, property).get().set().build();
             row.set_title(&gettext(key_data.summary().unwrap()));
             row.set_tooltip_markup(key_data.description().map(gettext).as_deref());
         }
@@ -179,9 +203,7 @@ impl Component for Preferences {
         root.connect_closed(glib::clone!(
             #[strong(rename_to = settings)]
             model.settings,
-            move |_| {
-                settings.apply()
-            }
+            move |_| { settings.apply() }
         ));
 
         ComponentParts { model, widgets }
diff --git a/src/components/signin.rs b/src/components/signin.rs
index f2b5313..972ab43 100644
--- a/src/components/signin.rs
+++ b/src/components/signin.rs
@@ -2,7 +2,9 @@ use gettextrs::*;
 use std::cell::RefCell;
 
 use adw::prelude::*;
-use kittybox_indieauth::{AuthorizationRequest, AuthorizationResponse, Error as IndieauthError, GrantResponse, Metadata};
+use kittybox_indieauth::{
+    AuthorizationRequest, AuthorizationResponse, Error as IndieauthError, GrantResponse, Metadata,
+};
 use relm4::prelude::*;
 use soup::prelude::{ServerExt, ServerExtManual, SessionExt};
 
@@ -33,7 +35,7 @@ pub struct SignIn {
     state: kittybox_indieauth::State,
     code_verifier: kittybox_indieauth::PKCEVerifier,
     micropub_uri: Option<glib::Uri>,
-    metadata: Option<Metadata>
+    metadata: Option<Metadata>,
 }
 
 #[derive(Debug, thiserror::Error)]
@@ -67,7 +69,10 @@ pub enum Input {
     Callback(Result<AuthorizationResponse, Error>),
 }
 
-pub async fn get_metadata(http: soup::Session, url: glib::Uri) -> Result<(Metadata, glib::Uri), Error> {
+pub async fn get_metadata(
+    http: soup::Session,
+    url: glib::Uri,
+) -> Result<(Metadata, glib::Uri), Error> {
     // Fire off a speculative request at the well-known URI. This could
     // improve UX by parallelizing how we query the user's website.
     //
@@ -75,15 +80,13 @@ pub async fn get_metadata(http: soup::Session, url: glib::Uri) -> Result<(Metada
     // RECOMMENDED though optional according to IndieAuth specification
     // ยงย 4.1.1, so we could use that if it's there to speed up the
     // process.
-    let metadata = relm4::spawn_local(
-        SignIn::well_known_metadata(http.clone(), url.clone())
-    );
+    let metadata = relm4::spawn_local(SignIn::well_known_metadata(http.clone(), url.clone()));
     let msg = soup::Message::from_uri("GET", &url);
     let body = http.send_future(&msg, glib::Priority::DEFAULT).await?;
 
     let mf2 = microformats::from_reader(
         std::io::BufReader::new(body.into_read()),
-        url.to_string().parse().unwrap()
+        url.to_string().parse().unwrap(),
     )?;
 
     let rels = mf2.rels.by_rels();
@@ -98,7 +101,7 @@ pub async fn get_metadata(http: soup::Session, url: glib::Uri) -> Result<(Metada
         // The new versions are superior by providing more features that
         // were previously proprietary extensions, and are more clearer in
         // general.
-        return Err(Error::MetadataNotFound)
+        return Err(Error::MetadataNotFound);
     };
 
     let micropub_uri = if let Some(url) = rels
@@ -108,26 +111,33 @@ pub async fn get_metadata(http: soup::Session, url: glib::Uri) -> Result<(Metada
     {
         glib::Uri::parse(url.as_str(), glib::UriFlags::NONE).unwrap()
     } else {
-        return Err(Error::MicropubLinkNotFound)
+        return Err(Error::MicropubLinkNotFound);
     };
 
     if let Ok(Some(metadata)) = metadata.await {
         Ok((metadata, micropub_uri))
     } else {
         let msg = soup::Message::from_uri("GET", &metadata_url);
-        msg.request_headers().unwrap().append("Accept", "application/json");
-        match http.send_and_read_future(&msg, glib::Priority::DEFAULT).await {
+        msg.request_headers()
+            .unwrap()
+            .append("Accept", "application/json");
+        match http
+            .send_and_read_future(&msg, glib::Priority::DEFAULT)
+            .await
+        {
             Ok(body) if msg.status() == soup::Status::Ok => {
                 let metadata = serde_json::from_slice(&body)?;
                 Ok((metadata, micropub_uri))
-            },
+            }
             Ok(_) => Err(Error::MetadataEndpointFailed(msg.status())),
             Err(err) => Err(err.into()),
         }
     }
 }
 
-fn callback_handler(sender: AsyncComponentSender<SignIn>) -> impl Fn(&soup::Server, &soup::ServerMessage, &str, std::collections::HashMap<&str, &str>) {
+fn callback_handler(
+    sender: AsyncComponentSender<SignIn>,
+) -> impl Fn(&soup::Server, &soup::ServerMessage, &str, std::collections::HashMap<&str, &str>) {
     move |server, msg, _, _| {
         let server = ObjectExt::downgrade(server);
         let sender = sender.clone();
@@ -148,7 +158,7 @@ fn callback_handler(sender: AsyncComponentSender<SignIn>) -> impl Fn(&soup::Serv
                 msg.set_response(
                     Some("text/plain; charset=\"utf-8\""),
                     soup::MemoryUse::Static,
-                    gettext("Thank you! This window can now be closed.").as_bytes()
+                    gettext("Thank you! This window can now be closed.").as_bytes(),
                 );
                 msg.connect_finished(move |_| {
                     sender.input(Input::Callback(Ok(response.take().unwrap())));
@@ -157,7 +167,7 @@ fn callback_handler(sender: AsyncComponentSender<SignIn>) -> impl Fn(&soup::Serv
                         soup::prelude::ServerExt::disconnect(&server);
                     }
                 });
-            },
+            }
             Err(err) => {
                 msg.set_status(400, soup::Status::phrase(400).as_deref());
                 if let Ok(err) = serde_urlencoded::from_str::<IndieauthError>(q.as_str()) {
@@ -191,15 +201,21 @@ impl SignIn {
         kittybox_indieauth::Scopes::new(vec![
             kittybox_indieauth::Scope::Profile,
             kittybox_indieauth::Scope::Create,
-            kittybox_indieauth::Scope::Media
+            kittybox_indieauth::Scope::Media,
         ])
     }
 
-    fn bail_out(&mut self, widgets: &mut <Self as AsyncComponent>::Widgets, sender: AsyncComponentSender<Self>, err: Error) {
-        widgets.toasts.add_toast(adw::Toast::builder()
-            .title(err.to_string())
-            .priority(adw::ToastPriority::High)
-            .build()
+    fn bail_out(
+        &mut self,
+        widgets: &mut <Self as AsyncComponent>::Widgets,
+        sender: AsyncComponentSender<Self>,
+        err: Error,
+    ) {
+        widgets.toasts.add_toast(
+            adw::Toast::builder()
+                .title(err.to_string())
+                .priority(adw::ToastPriority::High)
+                .build(),
         );
         // Reset all the state for the component for security reasons.
         self.busy_guard = None;
@@ -212,31 +228,45 @@ impl SignIn {
     }
 
     async fn well_known_metadata(http: soup::Session, url: glib::Uri) -> Option<Metadata> {
-        let well_known = url.parse_relative(
-            Self::WELL_KNOWN_METADATA_ENDPOINT_PATH,
-            glib::UriFlags::NONE
-        ).unwrap();
+        let well_known = url
+            .parse_relative(
+                Self::WELL_KNOWN_METADATA_ENDPOINT_PATH,
+                glib::UriFlags::NONE,
+            )
+            .unwrap();
         // Speculatively check for metadata at the well-known path
         let msg = soup::Message::from_uri("GET", &well_known);
-        msg.request_headers().unwrap().append("Accept", "application/json");
-        match http.send_and_read_future(&msg, glib::Priority::DEFAULT).await {
-            Ok(body) if msg.status() == soup::Status::Ok => {
-                match serde_json::from_slice(&body) {
-                    Ok(metadata) => {
-                        log::info!("Speculative metadata request successful: {:#?}", metadata);
-                        Some(metadata)
-                    },
-                    Err(err) => {
-                        log::warn!("Parsing OAuth2 metadata from {} failed: {}", well_known, err);
-                        None
-                    }
+        msg.request_headers()
+            .unwrap()
+            .append("Accept", "application/json");
+        match http
+            .send_and_read_future(&msg, glib::Priority::DEFAULT)
+            .await
+        {
+            Ok(body) if msg.status() == soup::Status::Ok => match serde_json::from_slice(&body) {
+                Ok(metadata) => {
+                    log::info!("Speculative metadata request successful: {:#?}", metadata);
+                    Some(metadata)
+                }
+                Err(err) => {
+                    log::warn!(
+                        "Parsing OAuth2 metadata from {} failed: {}",
+                        well_known,
+                        err
+                    );
+                    None
                 }
             },
             Ok(_) => {
-                log::warn!("Speculative request to {} returned {:?} ({})", well_known, msg.status(), msg.reason_phrase().unwrap());
+                log::warn!(
+                    "Speculative request to {} returned {:?} ({})",
+                    well_known,
+                    msg.status(),
+                    msg.reason_phrase().unwrap()
+                );
 
                 None
-            },
+            }
             Err(err) => {
                 log::warn!("Speculative request to {} failed: {}", well_known, err);
 
@@ -344,7 +374,13 @@ impl AsyncComponent for SignIn {
         std::future::ready(AsyncComponentParts { model, widgets })
     }
 
-    async fn update_with_view(&mut self, widgets: &mut Self::Widgets, msg: Self::Input, sender: AsyncComponentSender<Self>, _root: &Self::Root) {
+    async fn update_with_view(
+        &mut self,
+        widgets: &mut Self::Widgets,
+        msg: Self::Input,
+        sender: AsyncComponentSender<Self>,
+        _root: &Self::Root,
+    ) {
         match msg {
             Input::Start => {
                 self.busy_guard = Some(relm4::main_adw_application().mark_busy());
@@ -356,30 +392,25 @@ impl AsyncComponent for SignIn {
                     None => {
                         self.me_buffer.insert_text(0, "https://");
                         url = self.me_buffer.text().into();
-                    },
+                    }
                     Some(scheme) => {
                         if scheme != "https" && scheme != "http" {
-                            return self.bail_out(
-                                widgets, sender,
-                                Error::WrongScheme
-                            );
+                            return self.bail_out(widgets, sender, Error::WrongScheme);
                         }
-                    },
+                    }
                 }
                 let url = match glib::Uri::parse(url.as_str(), glib::UriFlags::SCHEME_NORMALIZE) {
                     Ok(url) => url,
                     Err(err) => {
-                        return self.bail_out(
-                            widgets, sender,
-                            err.into()
-                        );
-                    },
+                        return self.bail_out(widgets, sender, err.into());
+                    }
                 };
 
-                let (metadata, micropub_uri) = match get_metadata(self.http.clone(), url.clone()).await {
-                    Ok((metadata, micropub_uri)) => (metadata, micropub_uri),
-                    Err(err) => return self.bail_out(widgets, sender, err)
-                };
+                let (metadata, micropub_uri) =
+                    match get_metadata(self.http.clone(), url.clone()).await {
+                        Ok((metadata, micropub_uri)) => (metadata, micropub_uri),
+                        Err(err) => return self.bail_out(widgets, sender, err),
+                    };
 
                 let auth_request = AuthorizationRequest {
                     response_type: kittybox_indieauth::ResponseType::Code,
@@ -387,15 +418,17 @@ impl AsyncComponent for SignIn {
                     redirect_uri: REDIRECT_URI.parse().unwrap(),
                     state: self.state.clone(),
                     code_challenge: kittybox_indieauth::PKCEChallenge::new(
-                        &self.code_verifier, kittybox_indieauth::PKCEMethod::S256
+                        &self.code_verifier,
+                        kittybox_indieauth::PKCEMethod::S256,
                     ),
                     scope: Some(Self::scopes()),
-                    me: Some(url.to_str().parse().unwrap())
+                    me: Some(url.to_str().parse().unwrap()),
                 };
 
                 let auth_url = {
                     let mut url = metadata.authorization_endpoint.clone();
-                    url.query_pairs_mut().extend_pairs(auth_request.as_query_pairs());
+                    url.query_pairs_mut()
+                        .extend_pairs(auth_request.as_query_pairs());
 
                     url
                 };
@@ -407,40 +440,57 @@ impl AsyncComponent for SignIn {
                     server.add_handler(None, callback_handler(sender.clone()));
                     match server.listen_local(60000, soup::ServerListenOptions::empty()) {
                         Ok(()) => server,
-                        Err(err) => return self.bail_out(widgets, sender, err.into())
+                        Err(err) => return self.bail_out(widgets, sender, err.into()),
                     }
                 });
 
-                if let Err(err) = gtk::UriLauncher::new(auth_url.as_str()).launch_future(
-                    None::<&adw::ApplicationWindow>
-                ).await {
-                    return self.bail_out(widgets, sender, err.into())
+                if let Err(err) = gtk::UriLauncher::new(auth_url.as_str())
+                    .launch_future(None::<&adw::ApplicationWindow>)
+                    .await
+                {
+                    return self.bail_out(widgets, sender, err.into());
                 };
 
                 self.busy_guard = None;
                 self.update_view(widgets, sender);
-            },
+            }
             Input::Callback(Ok(res)) => {
                 // Immediately drop the event if we didn't take a server.
-                if self.callback_server.take().is_none() { return; }
+                if self.callback_server.take().is_none() {
+                    return;
+                }
                 self.busy_guard = Some(relm4::main_adw_application().mark_busy());
                 let metadata = self.metadata.take().unwrap();
                 let micropub_uri = self.micropub_uri.take().unwrap();
 
                 if res.state != self.state {
-                    return self.bail_out(widgets, sender, IndieauthError {
-                        kind: kittybox_indieauth::ErrorKind::InvalidRequest,
-                        msg: Some(gettext("state doesn't match what we remember, ceremony aborted")),
-                        error_uri: None,
-                    }.into())
+                    return self.bail_out(
+                        widgets,
+                        sender,
+                        IndieauthError {
+                            kind: kittybox_indieauth::ErrorKind::InvalidRequest,
+                            msg: Some(gettext(
+                                "state doesn't match what we remember, ceremony aborted",
+                            )),
+                            error_uri: None,
+                        }
+                        .into(),
+                    );
                 }
 
                 if res.iss != metadata.issuer {
-                    return self.bail_out(widgets, sender, IndieauthError {
-                        kind: kittybox_indieauth::ErrorKind::InvalidRequest,
-                        msg: Some(gettext("issuer doesn't match what we remember, ceremony aborted")),
-                        error_uri: None,
-                    }.into())
+                    return self.bail_out(
+                        widgets,
+                        sender,
+                        IndieauthError {
+                            kind: kittybox_indieauth::ErrorKind::InvalidRequest,
+                            msg: Some(gettext(
+                                "issuer doesn't match what we remember, ceremony aborted",
+                            )),
+                            error_uri: None,
+                        }
+                        .into(),
+                    );
                 }
 
                 let code = res.code;
@@ -450,21 +500,28 @@ impl AsyncComponent for SignIn {
                     redirect_uri: REDIRECT_URI.parse().unwrap(),
                     code_verifier: std::mem::replace(
                         &mut self.code_verifier,
-                        kittybox_indieauth::PKCEVerifier::new()
-                    )
+                        kittybox_indieauth::PKCEVerifier::new(),
+                    ),
                 };
 
-                let url = glib::Uri::parse(metadata.token_endpoint.as_str(), glib::UriFlags::NONE).unwrap();
+                let url = glib::Uri::parse(metadata.token_endpoint.as_str(), glib::UriFlags::NONE)
+                    .unwrap();
                 let msg = soup::Message::from_uri("POST", &url);
                 let headers = msg.request_headers().unwrap();
                 headers.append("Accept", "application/json");
                 msg.set_request_body_from_bytes(
                     Some("application/x-www-form-urlencoded"),
                     Some(&glib::Bytes::from_owned(
-                        serde_urlencoded::to_string(token_grant).unwrap().into_bytes()
-                    ))
+                        serde_urlencoded::to_string(token_grant)
+                            .unwrap()
+                            .into_bytes(),
+                    )),
                 );
-                match self.http.send_and_read_future(&msg, glib::Priority::DEFAULT).await {
+                match self
+                    .http
+                    .send_and_read_future(&msg, glib::Priority::DEFAULT)
+                    .await
+                {
                     Ok(body) if msg.status() == soup::Status::Ok => {
                         match serde_json::from_slice::<GrantResponse>(&body) {
                             Ok(GrantResponse::ProfileUrl(_)) => unreachable!(),
@@ -476,16 +533,16 @@ impl AsyncComponent for SignIn {
                                 state: _,
                                 expires_in,
                                 profile,
-                                refresh_token
+                                refresh_token,
                             }) => {
                                 let _ = sender.output(Output {
-                                    me: glib::Uri::parse(me.as_str(), glib::UriFlags::NONE).unwrap(),
+                                    me: glib::Uri::parse(me.as_str(), glib::UriFlags::NONE)
+                                        .unwrap(),
                                     scope: scope.unwrap_or_else(Self::scopes),
                                     micropub: micropub_uri,
-                                    userinfo: metadata.userinfo_endpoint
-                                        .map(|u| glib::Uri::parse(
-                                            u.as_str(), glib::UriFlags::NONE
-                                        ).unwrap()),
+                                    userinfo: metadata.userinfo_endpoint.map(|u| {
+                                        glib::Uri::parse(u.as_str(), glib::UriFlags::NONE).unwrap()
+                                    }),
                                     access_token,
                                     refresh_token,
                                     expires_in: expires_in.map(std::time::Duration::from_secs),
@@ -493,22 +550,18 @@ impl AsyncComponent for SignIn {
                                 });
                                 self.busy_guard = None;
                                 self.update_view(widgets, sender);
-                            },
+                            }
                             Err(err) => self.bail_out(widgets, sender, err.into()),
                         }
+                    }
+                    Ok(body) => match serde_json::from_slice::<IndieauthError>(&body) {
+                        Ok(err) => self.bail_out(widgets, sender, err.into()),
+                        Err(err) => self.bail_out(widgets, sender, err.into()),
                     },
-                    Ok(body) => {
-                        match serde_json::from_slice::<IndieauthError>(&body) {
-                            Ok(err) => self.bail_out(widgets, sender, err.into()),
-                            Err(err) => self.bail_out(widgets, sender, err.into())
-                        }
-                    },
-                    Err(err) => self.bail_out(widgets, sender, err.into())
+                    Err(err) => self.bail_out(widgets, sender, err.into()),
                 }
-            },
-            Input::Callback(Err(err)) => {
-                self.bail_out(widgets, sender, err)
-            },
+            }
+            Input::Callback(Err(err)) => self.bail_out(widgets, sender, err),
         }
     }
 
diff --git a/src/components/smart_summary.rs b/src/components/smart_summary.rs
index de6eb91..e876195 100644
--- a/src/components/smart_summary.rs
+++ b/src/components/smart_summary.rs
@@ -1,10 +1,14 @@
 #![cfg(feature = "smart-summary")]
+use adw::prelude::*;
 use futures::AsyncBufReadExt;
+use gettextrs::*;
 use gio::prelude::SettingsExtManual;
+use relm4::{
+    gtk,
+    prelude::{Component, ComponentParts},
+    ComponentSender,
+};
 use soup::prelude::*;
-use adw::prelude::*;
-use gettextrs::*;
-use relm4::{gtk, prelude::{Component, ComponentParts}, ComponentSender};
 
 // All of this is incredibly minimalist.
 // This should be expanded later.
@@ -23,7 +27,7 @@ pub(crate) struct OllamaChunk {
 
 #[derive(Debug, serde::Deserialize)]
 pub(crate) struct OllamaError {
-    error: String
+    error: String,
 }
 impl std::error::Error for OllamaError {}
 impl std::fmt::Display for OllamaError {
@@ -43,12 +47,11 @@ impl From<OllamaResult> for Result<OllamaChunk, OllamaError> {
     fn from(val: OllamaResult) -> Self {
         match val {
             OllamaResult::Ok(chunk) => Ok(chunk),
-            OllamaResult::Err(err) => Err(err)
+            OllamaResult::Err(err) => Err(err),
         }
     }
 }
 
-
 #[derive(Debug, Default)]
 pub(crate) struct SmartSummaryButton {
     task: Option<relm4::JoinHandle<()>>,
@@ -65,41 +68,48 @@ impl SmartSummaryButton {
     ) {
         let settings = gio::Settings::new(crate::APPLICATION_ID);
         // We shouldn't let the user record a bad setting anyway.
-        let endpoint = glib::Uri::parse(
-            &settings.string("llm-endpoint"),
-            glib::UriFlags::NONE,
-        ).unwrap();
+        let endpoint =
+            glib::Uri::parse(&settings.string("llm-endpoint"), glib::UriFlags::NONE).unwrap();
         let model = settings.get::<String>("smart-summary-model");
         let system_prompt = settings.get::<String>("smart-summary-system-prompt");
         let prompt_prefix = settings.get::<String>("smart-summary-prompt-prefix");
         let mut prompt_suffix = settings.get::<String>("smart-summary-prompt-suffix");
 
-        let endpoint = endpoint.parse_relative("./api/generate", glib::UriFlags::NONE).unwrap();
+        let endpoint = endpoint
+            .parse_relative("./api/generate", glib::UriFlags::NONE)
+            .unwrap();
         log::debug!("endpoint: {}, model: {}", endpoint, model);
         log::debug!("system prompt: {}", system_prompt);
 
-        let msg = soup::Message::from_uri(
-            "POST",
-            &endpoint
-        );
+        let msg = soup::Message::from_uri("POST", &endpoint);
 
         if !prompt_suffix.is_empty() {
             prompt_suffix = String::from("\n\n") + &prompt_suffix;
         }
-        msg.set_request_body_from_bytes(Some("application/json"),
-            Some(&glib::Bytes::from_owned(serde_json::to_vec(&OllamaRequest {
-                model, system: system_prompt, prompt: format!("{}\n\n{}{}", prompt_prefix, text, prompt_suffix),
-            }).unwrap()))
+        msg.set_request_body_from_bytes(
+            Some("application/json"),
+            Some(&glib::Bytes::from_owned(
+                serde_json::to_vec(&OllamaRequest {
+                    model,
+                    system: system_prompt,
+                    prompt: format!("{}\n\n{}{}", prompt_prefix, text, prompt_suffix),
+                })
+                .unwrap(),
+            )),
         );
 
         let mut stream = match http.send_future(&msg, glib::Priority::DEFAULT).await {
             Ok(stream) => stream.into_async_buf_read(128),
             Err(err) => {
                 let _ = sender.send(Err(err.into()));
-                return
+                return;
             }
         };
-        log::debug!("response: {:?} ({})", msg.status(), msg.reason_phrase().unwrap_or_default());
+        log::debug!(
+            "response: {:?} ({})",
+            msg.status(),
+            msg.reason_phrase().unwrap_or_default()
+        );
         let mut buffer = Vec::with_capacity(2048);
         const DELIM: u8 = b'\n';
         loop {
@@ -107,28 +117,36 @@ impl SmartSummaryButton {
                 Ok(len) => len,
                 Err(err) => {
                     let _ = sender.send(Err(err.into()));
-                    return
+                    return;
                 }
             };
-            log::debug!("Got chunk ({} bytes): {}", len, String::from_utf8_lossy(&buffer));
-            let response: Result<OllamaResult, serde_json::Error> = serde_json::from_slice(&buffer[..len]);
+            log::debug!(
+                "Got chunk ({} bytes): {}",
+                len,
+                String::from_utf8_lossy(&buffer)
+            );
+            let response: Result<OllamaResult, serde_json::Error> =
+                serde_json::from_slice(&buffer[..len]);
             match response.map(Result::from) {
-                Ok(Ok(OllamaChunk { response: chunk, done })) => {
+                Ok(Ok(OllamaChunk {
+                    response: chunk,
+                    done,
+                })) => {
                     if !chunk.is_empty() {
                         sender.emit(Ok(chunk));
                     }
                     if done {
                         sender.emit(Ok(String::new()));
-                        return
+                        return;
                     }
-                },
+                }
                 Ok(Err(err)) => {
                     sender.emit(Err(err.into()));
-                    return
+                    return;
                 }
                 Err(err) => {
                     sender.emit(Err(err.into()));
-                    return
+                    return;
                 }
             }
             buffer.truncate(0);
@@ -146,13 +164,15 @@ pub(crate) enum Error {
     #[allow(private_interfaces)]
     Ollama(#[from] OllamaError),
     #[error("i/o error: {0}")]
-    Io(#[from] std::io::Error)
+    Io(#[from] std::io::Error),
 }
 
 #[derive(Debug)]
 pub(crate) enum Input {
-    #[doc(hidden)] ButtonPressed,
-    #[doc(hidden)] WarningAccepted,
+    #[doc(hidden)]
+    ButtonPressed,
+    #[doc(hidden)]
+    WarningAccepted,
     Text(String),
     Cancel,
 }
@@ -163,7 +183,7 @@ pub(crate) enum Output {
     Chunk(String),
     Done,
 
-    Error(Error)
+    Error(Error),
 }
 
 #[relm4::component(pub(crate))]
@@ -198,7 +218,7 @@ impl Component for SmartSummaryButton {
     fn init(
         init: Self::Init,
         root: Self::Root,
-        sender: ComponentSender<Self>
+        sender: ComponentSender<Self>,
     ) -> ComponentParts<Self> {
         let model = Self {
             http: init,
@@ -209,12 +229,7 @@ impl Component for SmartSummaryButton {
         ComponentParts { model, widgets }
     }
 
-    fn update(
-        &mut self,
-        msg: Self::Input,
-        sender: ComponentSender<Self>,
-        _root: &Self::Root
-    ) {
+    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, _root: &Self::Root) {
         match msg {
             Input::Cancel => {
                 self.waiting = false;
@@ -222,23 +237,29 @@ impl Component for SmartSummaryButton {
                     log::debug!("Parent component asked us to cancel.");
                     task.abort();
                 } else {
-                    log::warn!("Parent component asked us to cancel, but we're not running a task.");
+                    log::warn!(
+                        "Parent component asked us to cancel, but we're not running a task."
+                    );
                 }
-            },
+            }
             Input::ButtonPressed => {
                 let settings = gio::Settings::new(crate::APPLICATION_ID);
                 if !settings.get::<bool>("smart-summary-show-warning") {
                     self.update(Input::WarningAccepted, sender, _root)
                 } else {
                     // TODO: show warning dialog
-                    let skip_warning_checkbox = gtk::CheckButton::with_label(
-                        &gettext("Show this warning next time")
-                    );
+                    let skip_warning_checkbox =
+                        gtk::CheckButton::with_label(&gettext("Show this warning next time"));
 
-                    settings.bind(
-                        "smart-summary-show-warning",
-                        &skip_warning_checkbox, "active"
-                    ).get().set().build();
+                    settings
+                        .bind(
+                            "smart-summary-show-warning",
+                            &skip_warning_checkbox,
+                            "active",
+                        )
+                        .get()
+                        .set()
+                        .build();
 
                     let dialog = adw::AlertDialog::builder()
                         .heading(gettext("LLMs can be deceiving"))
@@ -251,44 +272,51 @@ impl Component for SmartSummaryButton {
                         .build();
                     dialog.add_responses(&[
                         ("close", &gettext("Cancel")),
-                        ("continue", &gettext("Proceed"))
+                        ("continue", &gettext("Proceed")),
                     ]);
                     dialog.choose(
                         &_root.root().unwrap(),
                         None::<&gio::Cancellable>,
                         glib::clone!(
-                            #[strong] sender,
+                            #[strong]
+                            sender,
                             move |res| if res.as_str() == "continue" {
                                 sender.input(Input::WarningAccepted);
                             }
-                        ))
+                        ),
+                    )
+                }
+            }
+            Input::WarningAccepted => {
+                if let Ok(()) = sender.output(Output::Start) {
+                    self.waiting = true;
+                    log::debug!("Requesting text to summarize from parent component...");
+                    // TODO: set timeout in case parent component never replies
+                    // This shouldn't happen, but I feel like we should handle this case.
                 }
-            },
-            Input::WarningAccepted => if let Ok(()) = sender.output(Output::Start) {
-                self.waiting = true;
-                log::debug!("Requesting text to summarize from parent component...");
-                // TODO: set timeout in case parent component never replies
-                // This shouldn't happen, but I feel like we should handle this case.
-            },
+            }
             Input::Text(text) => {
                 log::debug!("Would generate summary for the following text:\n{}", text);
 
                 log::debug!("XDG_DATA_DIRS={:?}", std::env::var("XDG_DATA_DIRS"));
                 let sender = sender.command_sender().clone();
-                relm4::spawn_local(Self::summarize(
-                    sender, self.http.clone(), text
-                ));
+                relm4::spawn_local(Self::summarize(sender, self.http.clone(), text));
             }
         }
     }
 
-    fn update_cmd(&mut self, msg: Self::CommandOutput, sender: ComponentSender<Self>, _root: &Self::Root) {
+    fn update_cmd(
+        &mut self,
+        msg: Self::CommandOutput,
+        sender: ComponentSender<Self>,
+        _root: &Self::Root,
+    ) {
         match msg {
             Ok(chunk) if chunk.is_empty() => {
                 self.task = None;
                 self.waiting = false;
                 let _ = sender.output(Output::Done);
-            },
+            }
             Err(err) => {
                 self.task = None;
                 self.waiting = false;
@@ -296,7 +324,7 @@ impl Component for SmartSummaryButton {
             }
             Ok(chunk) => {
                 let _ = sender.output(Output::Chunk(chunk));
-            },
+            }
         }
     }
 }
diff --git a/src/components/tag_pill.rs b/src/components/tag_pill.rs
index 0dc9117..89b35af 100644
--- a/src/components/tag_pill.rs
+++ b/src/components/tag_pill.rs
@@ -70,8 +70,6 @@ impl FactoryComponent for TagPill {
         root.append(&label);
         root.append(&button);
 
-        Self::Widgets {
-            label, button
-        }
+        Self::Widgets { label, button }
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index fd4b51c..84379cc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,7 +1,16 @@
-use gettextrs::*;
 use adw::prelude::*;
-use libsecret::prelude::{RetrievableExtManual, RetrievableExt};
-use relm4::{actions::{RelmAction, RelmActionGroup}, gtk, loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};
+use gettextrs::*;
+use libsecret::prelude::{RetrievableExt, RetrievableExtManual};
+use relm4::{
+    actions::{RelmAction, RelmActionGroup},
+    gtk,
+    loading_widgets::LoadingWidgets,
+    prelude::{
+        AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController,
+        ComponentController, Controller,
+    },
+    AsyncComponentSender, Component, RelmWidgetExt,
+};
 
 pub mod icons {
     include!(concat!(env!("OUT_DIR"), "/icons.rs"));
@@ -11,29 +20,30 @@ pub mod components {
     pub(crate) mod smart_summary;
     #[cfg(feature = "smart-summary")]
     pub(crate) use smart_summary::{
-        SmartSummaryButton, Output as SmartSummaryOutput, Input as SmartSummaryInput
+        Input as SmartSummaryInput, Output as SmartSummaryOutput, SmartSummaryButton,
     };
 
     pub(crate) mod post_editor;
-    pub(crate) use post_editor::{
-        PostEditor, Input as PostEditorInput
-    };
+    pub(crate) use post_editor::{Input as PostEditorInput, PostEditor};
 
     pub(crate) mod tag_pill;
     // pub(crate) use tag_pill::{TagPill, TagPillDelete}
 
     pub mod signin;
-    pub use signin::{SignIn, Output as SignInOutput, Error as SignInError};
+    pub use signin::{Error as SignInError, Output as SignInOutput, SignIn};
 
     pub mod preferences;
     pub use preferences::Preferences;
 }
 
-use components::{post_editor::{Post, PostConversionSettings}, PostEditorInput};
+use components::{
+    post_editor::{Post, PostConversionSettings},
+    PostEditorInput,
+};
 use soup::prelude::SessionExt;
 
-pub mod secrets;
 pub mod micropub;
+pub mod secrets;
 pub mod util;
 pub const APPLICATION_ID: &str = env!("APP_ID");
 pub const CLIENT_ID_STR: &str = "https://kittybox.fireburn.ru/bowl/";
@@ -53,7 +63,11 @@ pub struct App {
 }
 
 impl App {
-    async fn authorize(schema: &libsecret::Schema, http: soup::Session, data: Box<components::SignInOutput>) -> Result<micropub::Client, glib::Error> {
+    async fn authorize(
+        schema: &libsecret::Schema,
+        http: soup::Session,
+        data: Box<components::SignInOutput>,
+    ) -> Result<micropub::Client, glib::Error> {
         let mut attributes = std::collections::HashMap::new();
         let me = data.me.to_string();
         let _micropub = data.micropub.to_string();
@@ -62,7 +76,8 @@ impl App {
         attributes.insert(secrets::TOKEN_KIND, secrets::ACCESS_TOKEN);
         attributes.insert(secrets::MICROPUB, _micropub.as_str());
         attributes.insert(secrets::SCOPE, scope.as_str());
-        let exp = data.expires_in
+        let exp = data
+            .expires_in
             .as_ref()
             .map(std::time::Duration::as_secs)
             .as_ref()
@@ -76,13 +91,15 @@ impl App {
             attributes.clone(),
             Some(libsecret::COLLECTION_DEFAULT),
             &gettext!("Micropub access token for {}", &data.me),
-            &data.access_token
-        ).await {
-            Ok(()) => {},
+            &data.access_token,
+        )
+        .await
+        {
+            Ok(()) => {}
             Err(err) => {
                 log::error!("Failed to store access token to the secret store: {}", err);
-                return Err(err)
-            },
+                return Err(err);
+            }
         }
         if let Some(refresh_token) = data.refresh_token.as_deref() {
             attributes.insert(secrets::TOKEN_KIND, secrets::REFRESH_TOKEN);
@@ -92,28 +109,42 @@ impl App {
                 attributes,
                 Some(libsecret::COLLECTION_DEFAULT),
                 &format!("Micropub refresh token for {}", &data.me),
-                refresh_token
-            ).await {
-                Ok(()) => {},
+                refresh_token,
+            )
+            .await
+            {
+                Ok(()) => {}
                 Err(err) => {
                     log::error!("Failed to store refresh token to the secret store: {}", err);
-                    return Err(err)
-                },
+                    return Err(err);
+                }
             }
         }
 
         Ok(micropub::Client::new(
-            http.clone(), data.micropub.clone(), data.access_token.clone(), me
+            http.clone(),
+            data.micropub.clone(),
+            data.access_token.clone(),
+            me,
         ))
     }
 
-    async fn refresh_token(schema: &libsecret::Schema, http: soup::Session, me: String) -> Result<Option<micropub::Client>, glib::Error> {
-        let mut retrievables = libsecret::password_search_future(Some(schema), {
-            let mut attrs = std::collections::HashMap::default();
-            attrs.insert(crate::secrets::TOKEN_KIND, crate::secrets::REFRESH_TOKEN);
-            attrs.insert(crate::secrets::ME, &me);
-            attrs
-        }, libsecret::SearchFlags::ALL).await?;
+    async fn refresh_token(
+        schema: &libsecret::Schema,
+        http: soup::Session,
+        me: String,
+    ) -> Result<Option<micropub::Client>, glib::Error> {
+        let mut retrievables = libsecret::password_search_future(
+            Some(schema),
+            {
+                let mut attrs = std::collections::HashMap::default();
+                attrs.insert(crate::secrets::TOKEN_KIND, crate::secrets::REFRESH_TOKEN);
+                attrs.insert(crate::secrets::ME, &me);
+                attrs
+            },
+            libsecret::SearchFlags::ALL,
+        )
+        .await?;
 
         if retrievables.is_empty() {
             Ok(None)
@@ -127,7 +158,9 @@ impl App {
                     .map(|(k, v)| (k.as_str(), v.as_str()))
                     .collect();
 
-                let token = retrievable.retrieve_secret_future().await?
+                let token = retrievable
+                    .retrieve_secret_future()
+                    .await?
                     .unwrap()
                     .text()
                     .unwrap()
@@ -135,30 +168,40 @@ impl App {
 
                 let url = glib::Uri::parse(me.as_str(), glib::UriFlags::SCHEME_NORMALIZE)?;
 
-                let (metadata, micropub_uri) = match crate::components::signin::get_metadata(http.clone(), url).await {
-                    Ok(res) => res,
-                    Err(err) => {
-                        tracing::warn!("failed to fetch metadata to refresh an expired token: {}", err);
-                        return Ok(None)
-                    }
-                };
+                let (metadata, micropub_uri) =
+                    match crate::components::signin::get_metadata(http.clone(), url).await {
+                        Ok(res) => res,
+                        Err(err) => {
+                            tracing::warn!(
+                                "failed to fetch metadata to refresh an expired token: {}",
+                                err
+                            );
+                            return Ok(None);
+                        }
+                    };
 
                 let grant = kittybox_indieauth::GrantRequest::RefreshToken {
-                    refresh_token: token, client_id: CLIENT_ID_STR.parse().unwrap(), scope: None
+                    refresh_token: token,
+                    client_id: CLIENT_ID_STR.parse().unwrap(),
+                    scope: None,
                 };
 
-                let url = glib::Uri::parse(metadata.token_endpoint.as_str(), glib::UriFlags::NONE).unwrap();
+                let url = glib::Uri::parse(metadata.token_endpoint.as_str(), glib::UriFlags::NONE)
+                    .unwrap();
                 let msg = soup::Message::from_uri("POST", &url);
                 let headers = msg.request_headers().unwrap();
                 headers.append("Accept", "application/json");
                 msg.set_request_body_from_bytes(
                     Some("application/x-www-form-urlencoded"),
                     Some(&glib::Bytes::from_owned(
-                        serde_urlencoded::to_string(grant).unwrap().into_bytes()
-                    ))
+                        serde_urlencoded::to_string(grant).unwrap().into_bytes(),
+                    )),
                 );
 
-                match http.send_and_read_future(&msg, glib::Priority::DEFAULT).await {
+                match http
+                    .send_and_read_future(&msg, glib::Priority::DEFAULT)
+                    .await
+                {
                     Ok(body) if msg.status() == soup::Status::Ok => {
                         match serde_json::from_slice::<kittybox_indieauth::GrantResponse>(&body) {
                             Ok(kittybox_indieauth::GrantResponse::ProfileUrl(_)) => unreachable!(),
@@ -170,81 +213,96 @@ impl App {
                                 state: _,
                                 expires_in,
                                 profile,
-                                refresh_token
+                                refresh_token,
                             }) => {
                                 if refresh_token.is_some() {
                                     // Get rid of the old refresh token.
-                                    let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;                                    
+                                    let _ =
+                                        libsecret::password_clear_future(Some(schema), attrs_ref)
+                                            .await;
                                 };
                                 let micropub = Self::authorize(
-                                    schema, http, Box::new(components::SignInOutput {
-                                        me: glib::Uri::parse(me.as_str(), glib::UriFlags::NONE).unwrap(),
+                                    schema,
+                                    http,
+                                    Box::new(components::SignInOutput {
+                                        me: glib::Uri::parse(me.as_str(), glib::UriFlags::NONE)
+                                            .unwrap(),
                                         scope: scope.unwrap_or_else(components::SignIn::scopes),
                                         micropub: micropub_uri,
-                                        userinfo: metadata.userinfo_endpoint
-                                            .map(|u| glib::Uri::parse(
-                                                u.as_str(), glib::UriFlags::NONE
-                                            ).unwrap()),
+                                        userinfo: metadata.userinfo_endpoint.map(|u| {
+                                            glib::Uri::parse(u.as_str(), glib::UriFlags::NONE)
+                                                .unwrap()
+                                        }),
                                         access_token,
                                         refresh_token,
                                         expires_in: expires_in.map(std::time::Duration::from_secs),
                                         profile,
-                                    })
-                                ).await?;
+                                    }),
+                                )
+                                .await?;
 
-                                return Ok(Some(micropub))
-                            },
+                                return Ok(Some(micropub));
+                            }
                             Err(err) => {
                                 tracing::warn!("failed to refresh token for {}: failed to parse grant response: {}", me, err);
-                                return Ok(None)
-                            },
-                        }
-                    },
-                    Ok(body) => {
-                        match serde_json::from_slice::<kittybox_indieauth::Error>(&body) {
-                            Ok(err) => {
-                                tracing::warn!("failed to refresh token for {}: token endpoint error: {}", me, err);
-                                continue;
-                            },
-                            Err(err) => {
-                                tracing::warn!("failed to refresh token for {}: error parsing token endpoint error: {}", me, err);
-                                tracing::warn!("token endpoint response verbatim follows:\n{}", String::from_utf8_lossy(&body));
-                                return Ok(None)
+                                return Ok(None);
                             }
                         }
+                    }
+                    Ok(body) => match serde_json::from_slice::<kittybox_indieauth::Error>(&body) {
+                        Ok(err) => {
+                            tracing::warn!(
+                                "failed to refresh token for {}: token endpoint error: {}",
+                                me,
+                                err
+                            );
+                            continue;
+                        }
+                        Err(err) => {
+                            tracing::warn!("failed to refresh token for {}: error parsing token endpoint error: {}", me, err);
+                            tracing::warn!(
+                                "token endpoint response verbatim follows:\n{}",
+                                String::from_utf8_lossy(&body)
+                            );
+                            return Ok(None);
+                        }
                     },
                     Err(err) => return Err(err),
                 };
-
             }
             unreachable!()
         }
     }
 
-    async fn revoke_token(http: soup::Session, me: String, token: String) -> Result<Option<()>, components::SignInError> {
-        let url = glib::Uri::parse(
-            me.as_str(),
-            glib::UriFlags::SCHEME_NORMALIZE
-        )?;
+    async fn revoke_token(
+        http: soup::Session,
+        me: String,
+        token: String,
+    ) -> Result<Option<()>, components::SignInError> {
+        let url = glib::Uri::parse(me.as_str(), glib::UriFlags::SCHEME_NORMALIZE)?;
 
         let (metadata, _) = crate::components::signin::get_metadata(http.clone(), url).await?;
 
         let endpoint = match metadata.revocation_endpoint {
             Some(endpoint) => match metadata.revocation_endpoint_auth_methods_supported {
-                Some(methods) => if methods.iter().any(|i| matches!(i, kittybox_indieauth::RevocationEndpointAuthMethod::None)) {
-                    glib::Uri::parse(endpoint.as_str(), glib::UriFlags::NONE).unwrap()
-                } else {
-                    tracing::warn!("couldn't revoke token: revocation endpoint doesn't support unauthenticated requests\n(Note: this is a violation of the IndieAuth specification)");
-                    return Ok(None)
-                },
+                Some(methods) => {
+                    if methods.iter().any(|i| {
+                        matches!(i, kittybox_indieauth::RevocationEndpointAuthMethod::None)
+                    }) {
+                        glib::Uri::parse(endpoint.as_str(), glib::UriFlags::NONE).unwrap()
+                    } else {
+                        tracing::warn!("couldn't revoke token: revocation endpoint doesn't support unauthenticated requests\n(Note: this is a violation of the IndieAuth specification)");
+                        return Ok(None);
+                    }
+                }
                 None => {
                     tracing::warn!("couldn't revoke token: revocation endpoint doesn't support unauthenticated requests\n(Note: this is a violation of the IndieAuth specification)");
-                    return Ok(None)
+                    return Ok(None);
                 }
             },
             None => {
                 tracing::warn!("couldn't revoke token: revocation endpoint not found");
-                return Ok(None)
+                return Ok(None);
             }
         };
         let msg = soup::Message::from_uri("POST", &endpoint);
@@ -253,37 +311,55 @@ impl App {
         msg.set_request_body_from_bytes(
             Some("application/x-www-form-urlencoded"),
             Some(&glib::Bytes::from_owned(
-                serde_urlencoded::to_string(
-                    kittybox_indieauth::TokenRevocationRequest { token }
-                ).unwrap().into_bytes()
-            ))
+                serde_urlencoded::to_string(kittybox_indieauth::TokenRevocationRequest { token })
+                    .unwrap()
+                    .into_bytes(),
+            )),
         );
 
-        match http.send_and_read_future(&msg, glib::Priority::DEFAULT).await {
-            Ok(_) if msg.status() == soup::Status::Ok => {
-                Ok(Some(()))
-            },
+        match http
+            .send_and_read_future(&msg, glib::Priority::DEFAULT)
+            .await
+        {
+            Ok(_) if msg.status() == soup::Status::Ok => Ok(Some(())),
             Ok(body) => {
-                tracing::warn!("couldn't revoke token: revocation endpoint returned non-200: {:?}", msg.status());
+                tracing::warn!(
+                    "couldn't revoke token: revocation endpoint returned non-200: {:?}",
+                    msg.status()
+                );
                 match serde_json::from_slice::<kittybox_indieauth::Error>(&body) {
                     Ok(err) => tracing::warn!("revocation endpoint returned an error: {}", err),
-                    Err(_) => tracing::warn!("couldn't parse revocation endpoint error, response verbatim follows:\n{}", String::from_utf8_lossy(&body))
+                    Err(_) => tracing::warn!(
+                        "couldn't parse revocation endpoint error, response verbatim follows:\n{}",
+                        String::from_utf8_lossy(&body)
+                    ),
                 }
                 Ok(None)
-            },
+            }
             Err(err) => {
-                tracing::warn!("couldn't revoke token: error contacting revocation endpoint: {:?}", err);
+                tracing::warn!(
+                    "couldn't revoke token: error contacting revocation endpoint: {:?}",
+                    err
+                );
                 Err(err.into())
             }
         }
     }
 
-    async fn get_login_state(schema: &libsecret::Schema, http: soup::Session) -> Result<Option<micropub::Client>, glib::Error> {
-        let mut retrievables = libsecret::password_search_future(Some(schema), {
-            let mut attrs = std::collections::HashMap::default();
-            attrs.insert(crate::secrets::TOKEN_KIND, crate::secrets::ACCESS_TOKEN);
-            attrs
-        }, libsecret::SearchFlags::ALL).await?;
+    async fn get_login_state(
+        schema: &libsecret::Schema,
+        http: soup::Session,
+    ) -> Result<Option<micropub::Client>, glib::Error> {
+        let mut retrievables = libsecret::password_search_future(
+            Some(schema),
+            {
+                let mut attrs = std::collections::HashMap::default();
+                attrs.insert(crate::secrets::TOKEN_KIND, crate::secrets::ACCESS_TOKEN);
+                attrs
+            },
+            libsecret::SearchFlags::ALL,
+        )
+        .await?;
 
         if retrievables.is_empty() {
             Ok(None)
@@ -298,26 +374,27 @@ impl App {
                     .collect();
                 let micropub_uri = match attrs
                     .get(crate::secrets::MICROPUB)
-                    .and_then(|v| glib::Uri::parse(
-                        v, glib::UriFlags::NONE
-                    ).ok()) {
-                        Some(uri) => uri,
-                        None => {
-                            let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;
-                            continue
-                        },
-                    };
+                    .and_then(|v| glib::Uri::parse(v, glib::UriFlags::NONE).ok())
+                {
+                    Some(uri) => uri,
+                    None => {
+                        let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;
+                        continue;
+                    }
+                };
 
                 let me = attrs.get(crate::secrets::ME).unwrap().to_string();
                 let micropub = crate::micropub::Client::new(
                     http.clone(),
                     micropub_uri,
-                    retrievable.retrieve_secret_future().await?
+                    retrievable
+                        .retrieve_secret_future()
+                        .await?
                         .unwrap()
                         .text()
                         .unwrap()
                         .to_string(),
-                    me.clone()
+                    me.clone(),
                 );
 
                 // Skip the token if we can't access ?q=config
@@ -327,21 +404,22 @@ impl App {
                         // Token may have expired. See if we have a refresh token and renew.
                         let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;
 
-                        match Self::refresh_token(
-                            schema, http.clone(),
-                            me.clone()
-                        ).await {
+                        match Self::refresh_token(schema, http.clone(), me.clone()).await {
                             Ok(None) => continue,
                             Err(err) => {
-                                tracing::warn!("error refreshing Micropub token for {}: {}", &me, err);
-                                continue
-                            },
-                            Ok(Some(micropub)) => return Ok(Some(micropub))
+                                tracing::warn!(
+                                    "error refreshing Micropub token for {}: {}",
+                                    &me,
+                                    err
+                                );
+                                continue;
+                            }
+                            Ok(Some(micropub)) => return Ok(Some(micropub)),
                         }
                     }
                 }
 
-                return Ok(Some(micropub))
+                return Ok(Some(micropub));
             }
 
             Ok(None)
@@ -461,12 +539,17 @@ impl AsyncComponent for App {
     ) -> AsyncComponentParts<Self> {
         let secret_schema = crate::secrets::get_schema();
         let http = soup::Session::builder()
-            .user_agent(concat!(env!("CARGO_PKG_NAME"),"/",env!("CARGO_PKG_VERSION")," "))
+            .user_agent(concat!(
+                env!("CARGO_PKG_NAME"),
+                "/",
+                env!("CARGO_PKG_VERSION"),
+                " "
+            ))
             .build();
 
-        let state = App::get_login_state(
-            &secret_schema, http.clone()
-        ).await.unwrap();
+        let state = App::get_login_state(&secret_schema, http.clone())
+            .await
+            .unwrap();
 
         let model = App {
             submit_busy_guard: None,
@@ -487,13 +570,13 @@ impl AsyncComponent for App {
                     .forward(sender.input_sender(), Self::Input::PostEditor)
             },
             signin: components::SignIn::builder()
-                .launch((glib::Uri::parse(
-                    CLIENT_ID_STR, glib::UriFlags::NONE
-                ).unwrap(), http))
-                .forward(
-                    sender.input_sender(),
-                    |o| Self::Input::Authorize(Box::new(o))
-                )
+                .launch((
+                    glib::Uri::parse(CLIENT_ID_STR, glib::UriFlags::NONE).unwrap(),
+                    http,
+                ))
+                .forward(sender.input_sender(), |o| {
+                    Self::Input::Authorize(Box::new(o))
+                }),
         };
 
         let widgets = view_output!();
@@ -504,20 +587,18 @@ impl AsyncComponent for App {
             App::about().present(weak_window.upgrade().as_ref());
         });
         let weak_window = window.downgrade();
-        let preferences_action: RelmAction<PreferencesAction> = RelmAction::new_stateless(move |_| {
-            // This could be built as an action that sends an input to open preferences.
-            //
-            // But I find this an acceptable alternative.
-            let mut prefs = components::Preferences::builder()
-                .launch(())
-                .detach();
-
-            prefs.emit(weak_window.upgrade().map(|w| w.upcast()));
-            prefs.detach_runtime();
-        });
-        let sign_out_action: RelmAction<SignOutAction> = RelmAction::new_stateless(move |_| {
-            input_sender.emit(Input::SignOut)
-        });
+        let preferences_action: RelmAction<PreferencesAction> =
+            RelmAction::new_stateless(move |_| {
+                // This could be built as an action that sends an input to open preferences.
+                //
+                // But I find this an acceptable alternative.
+                let mut prefs = components::Preferences::builder().launch(()).detach();
+
+                prefs.emit(weak_window.upgrade().map(|w| w.upcast()));
+                prefs.detach_runtime();
+            });
+        let sign_out_action: RelmAction<SignOutAction> =
+            RelmAction::new_stateless(move |_| input_sender.emit(Input::SignOut));
         let mut action_group: RelmActionGroup<AppActionGroup> = RelmActionGroup::new();
         action_group.add_action(about_action);
         action_group.add_action(preferences_action);
@@ -527,12 +608,11 @@ impl AsyncComponent for App {
         AsyncComponentParts { model, widgets }
     }
 
-
     async fn update(
         &mut self,
         message: Self::Input,
         _sender: AsyncComponentSender<Self>,
-        _root: &Self::Root
+        _root: &Self::Root,
     ) {
         match message {
             Input::SignOut => {
@@ -540,37 +620,43 @@ impl AsyncComponent for App {
                     let _ = libsecret::password_clear_future(
                         Some(&self.secret_schema),
                         Default::default(),
-                    ).await;
-                    let _ = Self::revoke_token(
-                        self.http.clone(), micropub.me, micropub.access_token
-                    ).await;
+                    )
+                    .await;
+                    let _ =
+                        Self::revoke_token(self.http.clone(), micropub.me, micropub.access_token)
+                            .await;
                 }
-            },
+            }
             Input::Authorize(data) => {
-                if let Ok(micropub) = Self::authorize(&self.secret_schema, self.http.clone(), data).await {
+                if let Ok(micropub) =
+                    Self::authorize(&self.secret_schema, self.http.clone(), data).await
+                {
                     self.micropub = Some(micropub);
                 }
-            },
+            }
             Input::SubmitButtonPressed => {
                 if self.micropub.is_some() {
                     self.submit_busy_guard = Some(relm4::main_adw_application().mark_busy());
                     // TODO: too easy to deadlock here, refactor to take a channel
                     self.post_editor.emit(PostEditorInput::Submit);
                 };
-            },
+            }
             Input::PostEditor(None) => {
                 self.submit_busy_guard = None;
             }
             Input::PostEditor(Some(post)) => {
                 if let Some(micropub) = self.micropub.as_ref() {
                     let mf2 = post.into_mf2(PostConversionSettings {
-                        send_html_directly: self.settings.get("send-html-directly")
+                        send_html_directly: self.settings.get("send-html-directly"),
                     });
-                    log::debug!("Submitting post: {:#}", serde_json::to_string(&mf2).unwrap());
+                    log::debug!(
+                        "Submitting post: {:#}",
+                        serde_json::to_string(&mf2).unwrap()
+                    );
                     match micropub.send_post(mf2).await {
                         Ok(uri) => {
                             self.post_editor.emit(PostEditorInput::SubmitDone(uri));
-                        },
+                        }
                         Err(err) => {
                             log::warn!("Error sending post: {}", err);
                             self.post_editor.emit(PostEditorInput::SubmitError(err));
@@ -578,7 +664,7 @@ impl AsyncComponent for App {
                     }
                 }
                 self.submit_busy_guard = None;
-            },
+            }
         }
     }
 }
diff --git a/src/main.rs b/src/main.rs
index 989516a..536563e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,10 +4,8 @@ static GLIB_LOGGER: glib::GlibLogger = glib::GlibLogger::new(
 );
 
 fn main() {
-    gettextrs::bindtextdomain(
-        env!("CARGO_PKG_NAME"),
-        env!("LOCALEDIR")
-    ).expect("failed to bind text domain");
+    gettextrs::bindtextdomain(env!("CARGO_PKG_NAME"), env!("LOCALEDIR"))
+        .expect("failed to bind text domain");
     gettextrs::bind_textdomain_codeset(env!("CARGO_PKG_NAME"), "UTF-8").unwrap();
     gettextrs::textdomain(env!("CARGO_PKG_NAME")).unwrap();
 
@@ -20,7 +18,8 @@ fn main() {
     spelling::init();
 
     let app = relm4::RelmApp::new(bowl::APPLICATION_ID);
-    relm4::set_global_css("/* CSS for Bowl */
+    relm4::set_global_css(
+        "/* CSS for Bowl */
 .tag-pill button {
     min-height: 30px;
     min-width: 30px;
@@ -28,7 +27,8 @@ fn main() {
 .tag-pill label {
     font-variant-caps: small-caps;
 }
-");
+",
+    );
 
     app.run_async::<bowl::App>(());
 }
diff --git a/src/micropub.rs b/src/micropub.rs
index b2f1e73..f87feb7 100644
--- a/src/micropub.rs
+++ b/src/micropub.rs
@@ -1,5 +1,5 @@
+pub use kittybox_util::micropub::{Config, Error as MicropubError, QueryType};
 use soup::prelude::*;
-pub use kittybox_util::micropub::{Error as MicropubError, Config, QueryType};
 
 #[derive(Debug)]
 pub struct Client {
@@ -19,7 +19,7 @@ pub enum Error {
     #[error("micropub error: {0}")]
     Micropub(#[from] MicropubError),
     #[error("micropub server did not return a location: header")]
-    NoLocationHeader
+    NoLocationHeader,
 }
 
 impl Client {
@@ -34,18 +34,21 @@ impl Client {
 
     pub async fn config(&self) -> Result<Config, Error> {
         let uri = glib::Uri::parse(&self.micropub, glib::UriFlags::NONE).unwrap();
-        let uri = super::util::append_query(
-            &uri, [("q".to_string(), "config".to_string())]
-        );
-        
+        let uri = super::util::append_query(&uri, [("q".to_string(), "config".to_string())]);
+
         let exch = soup::Message::from_uri("GET", &uri);
         let headers = exch.request_headers().expect("SoupMessage with no headers");
         // TODO: create a SoupAuth subclass that allows pasting in a token
         headers.append("Authorization", &format!("Bearer {}", self.access_token));
 
-        let body = self.http.send_and_read_future(&exch, glib::Priority::DEFAULT).await?;
+        let body = self
+            .http
+            .send_and_read_future(&exch, glib::Priority::DEFAULT)
+            .await?;
         if exch.status() == soup::Status::Unauthorized {
-            return Err(MicropubError::from(kittybox_util::micropub::ErrorKind::NotAuthorized).into())
+            return Err(
+                MicropubError::from(kittybox_util::micropub::ErrorKind::NotAuthorized).into(),
+            );
         }
 
         Ok(serde_json::from_slice(&body)?)
@@ -57,22 +60,32 @@ impl Client {
         let headers = exch.request_headers().expect("SoupMessage with no headers");
         headers.append("Authorization", &format!("Bearer {}", self.access_token));
 
-        exch.set_request_body_from_bytes(Some("application/json"),
-            Some(&glib::Bytes::from_owned(serde_json::to_vec(&post).unwrap()))
+        exch.set_request_body_from_bytes(
+            Some("application/json"),
+            Some(&glib::Bytes::from_owned(serde_json::to_vec(&post).unwrap())),
         );
 
-        let body = self.http.send_and_read_future(&exch, glib::Priority::DEFAULT).await?;
+        let body = self
+            .http
+            .send_and_read_future(&exch, glib::Priority::DEFAULT)
+            .await?;
 
         match exch.status() {
             soup::Status::Created | soup::Status::Accepted => {
-                let response_headers = exch.response_headers().expect("Successful SoupMessage with no response headers");
-                let location = response_headers.one("Location").ok_or(Error::NoLocationHeader)?;
+                let response_headers = exch
+                    .response_headers()
+                    .expect("Successful SoupMessage with no response headers");
+                let location = response_headers
+                    .one("Location")
+                    .ok_or(Error::NoLocationHeader)?;
 
                 Ok(glib::Uri::parse(&location, glib::UriFlags::NONE)?)
-            },
-            soup::Status::InternalServerError | soup::Status::BadGateway | soup::Status::ServiceUnavailable => {
+            }
+            soup::Status::InternalServerError
+            | soup::Status::BadGateway
+            | soup::Status::ServiceUnavailable => {
                 todo!("micropub server is down")
-            },
+            }
             soup::Status::Unauthorized => {
                 Err(MicropubError::from(kittybox_util::micropub::ErrorKind::NotAuthorized).into())
             }
@@ -80,7 +93,10 @@ impl Client {
                 let error = match serde_json::from_slice::<MicropubError>(&body) {
                     Ok(error) => error,
                     Err(err) => {
-                        tracing::debug!("Error serializing body: {}", String::from_utf8_lossy(&body));
+                        tracing::debug!(
+                            "Error serializing body: {}",
+                            String::from_utf8_lossy(&body)
+                        );
                         Err(err)?
                     }
                 };
diff --git a/src/secrets.rs b/src/secrets.rs
index fa74aa5..7763e5f 100644
--- a/src/secrets.rs
+++ b/src/secrets.rs
@@ -15,5 +15,9 @@ pub fn get_schema() -> libsecret::Schema {
     attrs.insert(EXPIRES_IN, libsecret::SchemaAttributeType::Integer);
     attrs.insert(SCOPE, libsecret::SchemaAttributeType::String);
 
-    libsecret::Schema::new("org.indieweb.indieauth.BearerCredential", libsecret::SchemaFlags::NONE, attrs)
+    libsecret::Schema::new(
+        "org.indieweb.indieauth.BearerCredential",
+        libsecret::SchemaFlags::NONE,
+        attrs,
+    )
 }
diff --git a/src/util.rs b/src/util.rs
index c3d5bd7..83d8e2b 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -1,7 +1,8 @@
 use std::borrow::Cow;
 
 pub fn append_query(uri: &glib::Uri, q: impl IntoIterator<Item = (String, String)>) -> glib::Uri {
-    let mut oq: Vec<(Cow<'static, str>, Cow<'static, str>)> = uri.query()
+    let mut oq: Vec<(Cow<'static, str>, Cow<'static, str>)> = uri
+        .query()
         .map(|q| serde_urlencoded::from_str(&q).unwrap())
         .unwrap_or_default();
     oq.extend(q.into_iter().map(|(k, v)| (k.into(), v.into())));
@@ -13,7 +14,10 @@ pub fn append_query(uri: &glib::Uri, q: impl IntoIterator<Item = (String, String
 mod tests {
     #[test]
     fn test_append_query() -> Result<(), glib::Error> {
-        let uri = glib::Uri::parse("https://fireburn.ru/.kittybox/micropub?test=a", glib::UriFlags::NONE)?;
+        let uri = glib::Uri::parse(
+            "https://fireburn.ru/.kittybox/micropub?test=a",
+            glib::UriFlags::NONE,
+        )?;
         let q = [
             ("q".to_owned(), "config".to_owned()),
             ("awoo".to_owned(), "nya".to_owned()),