diff options
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | default.nix | 10 | ||||
-rw-r--r-- | flake.nix | 4 | ||||
-rw-r--r-- | meson.options | 7 | ||||
-rw-r--r-- | src/components/post_editor.rs | 45 | ||||
-rw-r--r-- | src/components/preferences.rs | 137 | ||||
-rw-r--r-- | src/components/smart_summary.rs | 1 | ||||
-rw-r--r-- | src/lib.rs | 15 | ||||
-rw-r--r-- | src/meson.build | 6 |
9 files changed, 165 insertions, 64 deletions
diff --git a/Cargo.toml b/Cargo.toml index 13b6aa2..b1e2933 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,10 @@ authors = ["Vika <vika@fireburn.ru>"] license = "AGPL-3.0-only" edition = "2021" +[features] +smart-summary = [] +default = ["smart-summary"] + [dependencies] adw = { version = "0.7.0", package = "libadwaita", features = ["v1_5"] } futures = "0.3.30" diff --git a/default.nix b/default.nix index d406776..881b656 100644 --- a/default.nix +++ b/default.nix @@ -3,6 +3,8 @@ , desktop-file-utils , gtk4, libadwaita, libpanel, libsoup_3, libsecret , librsvg, glib-networking + +, withLLMEnhancements ? true }: let @@ -29,8 +31,6 @@ let }; strictDeps = true; - # cargoExtraArgs can be used to inject features - buildInputs = [ gtk4 libadwaita libsoup_3 libsecret librsvg glib-networking @@ -43,7 +43,9 @@ let platforms = ["aarch64-linux" "x86_64-linux"]; mainProgram = "bowl"; }; - }; + } // (lib.optionalAttrs (!withLLMEnhancements) { + cargoExtraArgs = lib.optionalString (!withLLMEnhancements) "--no-default-features"; + }); cargoArtifacts = craneLib.buildDepsOnly args; args' = args // { inherit cargoArtifacts; }; @@ -69,6 +71,8 @@ in craneLib.mkCargoDerivation (args' // { checkPhase = "mesonCheckPhase"; installPhase = "mesonInstallPhase"; + mesonFlags = lib.optional (!withLLMEnhancements) "-Dllm=false"; + nativeBuildInputs = args'.nativeBuildInputs ++ [ rustc # Only needed for Meson to successfully detect the Rust toolchain diff --git a/flake.nix b/flake.nix index c190706..4b78a24 100644 --- a/flake.nix +++ b/flake.nix @@ -31,6 +31,9 @@ in { packages = { bowl = bowl; + bowl-no-llm = bowl.override { + withLLMEnhancements = false; + }; default = self.packages.${system}.bowl; # Needed for translations xtr = pkgs.callPackage ./pkgs/xtr {}; @@ -38,6 +41,7 @@ checks = { bowl = self.packages.${system}.bowl; + bowl-no-llm = self.packages.${system}.bowl-no-llm; deps = self.packages.${system}.bowl.cargoArtifacts; clippy = self.packages.${system}.bowl.clippy; diff --git a/meson.options b/meson.options index 7decbf0..2b9608d 100644 --- a/meson.options +++ b/meson.options @@ -7,4 +7,11 @@ option( ], value: 'default', description: 'The build profile for Bowl. One of "default" or "development".' +) + +option( + 'llm', + type: 'boolean', + value: true, + description: 'Whether to enable optional LLM enhancement features.' ) \ No newline at end of file diff --git a/src/components/post_editor.rs b/src/components/post_editor.rs index f2268ad..c42b06a 100644 --- a/src/components/post_editor.rs +++ b/src/components/post_editor.rs @@ -1,11 +1,12 @@ use gettextrs::*; -use crate::components; use crate::components::tag_pill::*; use adw::prelude::*; use glib::translate::IntoGlib; use gtk::GridLayoutChild; -use relm4::{gtk, prelude::{ComponentController, Controller, DynamicIndex}, factory::FactoryVecDeque, Component, ComponentParts, ComponentSender, RelmWidgetExt}; +use relm4::{factory::FactoryVecDeque, gtk, prelude::{Controller, DynamicIndex}, Component, ComponentParts, ComponentSender, RelmWidgetExt}; +#[cfg(feature = "smart-summary")] +use relm4::prelude::ComponentController; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] #[enum_type(name = "MicropubVisibility")] @@ -87,7 +88,8 @@ pub(crate) struct PostEditor<E> { #[do_not_track] wide_layout: gtk::GridLayout, - #[do_not_track] smart_summary: Controller<components::SmartSummaryButton>, + #[cfg(feature = "smart-summary")] + #[do_not_track] smart_summary: Controller<crate::components::SmartSummaryButton>, _err: std::marker::PhantomData<E> } @@ -104,7 +106,8 @@ impl<E> PostEditor<E> { #[allow(private_interfaces)] // intentional #[allow(clippy::manual_non_exhaustive)] // false positive pub enum Input<E: std::error::Error + std::fmt::Debug + Send + 'static> { - #[doc(hidden)] SmartSummary(components::smart_summary::Output), + #[cfg(feature = "smart-summary")] + #[doc(hidden)] SmartSummary(crate::components::smart_summary::Output), #[doc(hidden)] VisibilitySelected(Visibility), #[doc(hidden)] AddTagFromBuffer, #[doc(hidden)] RemoveTag(DynamicIndex), @@ -115,7 +118,10 @@ pub enum Input<E: std::error::Error + std::fmt::Debug + Send + 'static> { #[relm4::component(pub)] impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for PostEditor<E> { - type Init = (<components::SmartSummaryButton as relm4::Component>::Init, Option<Post>); + #[cfg(feature = "smart-summary")] + type Init = (soup::Session, Option<Post>); + #[cfg(not(feature = "smart-summary"))] + type Init = Option<Post>; type Output = Option<Post>; type Input = Input<E>; type CommandOutput = (); @@ -169,8 +175,6 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post #[track = "model.busy_changed()"] set_sensitive: !model.busy(), }, - - model.smart_summary.widget(), }, #[name = "tag_label"] @@ -331,10 +335,13 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post } fn init( - (http, init): Self::Init, + init: Self::Init, root: Self::Root, sender: ComponentSender<Self> ) -> ComponentParts<Self> { + #[cfg(feature = "smart-summary")] + let (http, init) = init; + let mut model = Self { smart_summary_busy_guard: None, sending: false, @@ -359,7 +366,8 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post wide_layout: gtk::GridLayout::new(), - smart_summary: components::SmartSummaryButton::builder() + #[cfg(feature = "smart-summary")] + smart_summary: crate::components::SmartSummaryButton::builder() .launch(http) .forward(sender.input_sender(), Input::SmartSummary), @@ -371,6 +379,9 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post let widgets = view_output!(); + #[cfg(feature = "smart-summary")] + widgets.summary_field.append(model.smart_summary.widget()); + widgets.visibility_selector.set_expression(Some( gtk::ClosureExpression::new::<String>( [] as [gtk::Expression; 0], @@ -459,11 +470,12 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post fn update_with_view(&mut self, widgets: &mut Self::Widgets, msg: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) { self.reset(); match msg { - Input::SmartSummary(components::SmartSummaryOutput::Start) => { + #[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( - components::SmartSummaryInput::Cancel + crate::components::SmartSummaryInput::Cancel ); } else { let text = self.content_buffer.text( @@ -476,20 +488,23 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post Some(relm4::main_adw_application().mark_busy()) ); if self.smart_summary.sender().send( - components::SmartSummaryInput::Text(text.into()) + crate::components::SmartSummaryInput::Text(text.into()) ).is_ok() { self.summary_buffer.set_text(""); } } widgets.content_textarea.set_sensitive(true); }, - Input::SmartSummary(components::SmartSummaryOutput::Chunk(text)) => { + #[cfg(feature = "smart-summary")] + Input::SmartSummary(crate::components::SmartSummaryOutput::Chunk(text)) => { self.summary_buffer.insert_text(self.summary_buffer.length(), text); }, - Input::SmartSummary(components::SmartSummaryOutput::Done) => { + #[cfg(feature = "smart-summary")] + Input::SmartSummary(crate::components::SmartSummaryOutput::Done) => { self.set_smart_summary_busy_guard(None); }, - Input::SmartSummary(components::SmartSummaryOutput::Error(err)) => { + #[cfg(feature = "smart-summary")] + Input::SmartSummary(crate::components::SmartSummaryOutput::Error(err)) => { self.set_smart_summary_busy_guard(None); let toast = adw::Toast::new(&gettext!("Smart Summary error: {}", err)); diff --git a/src/components/preferences.rs b/src/components/preferences.rs index 9bbc313..fbf406d 100644 --- a/src/components/preferences.rs +++ b/src/components/preferences.rs @@ -1,4 +1,3 @@ -use gettextrs::*; use gio::prelude::*; use adw::prelude::*; use relm4::prelude::*; @@ -7,38 +6,100 @@ pub struct Preferences { settings: gio::Settings, } -#[relm4::component(pub)] +#[cfg(feature = "smart-summary")] +#[allow(dead_code)] +struct LanguageModelPreferencesWidgets { + page: adw::PreferencesPage, + + general_group: adw::PreferencesGroup, + llm_endpoint: adw::EntryRow, + + smart_summary_group: adw::PreferencesGroup, + smart_summary_model: adw::EntryRow, + smart_summary_system_prompt: adw::EntryRow, + smart_summary_prompt_prefix: adw::EntryRow, + smart_summary_prompt_suffix: adw::EntryRow, +} + +#[cfg(feature = "smart-summary")] +impl LanguageModelPreferencesWidgets { + fn new(settings: &gio::Settings) -> Self { + use gettextrs::*; + + let page = adw::PreferencesPage::builder() + .title(gettext("Language Models")) + .description(gettext("Settings for the language model integrations.")) + .icon_name("magic-wand") + .build(); + + let general_group = adw::PreferencesGroup::builder() + .title(gettext("General")) + .build(); + let llm_endpoint = adw::EntryRow::new(); + general_group.add(&llm_endpoint); + page.add(&general_group); + + let smart_summary_group = adw::PreferencesGroup::builder() + .title(gettext("Smart Summary")) + .build(); + let smart_summary_model = adw::EntryRow::new(); + let smart_summary_system_prompt = adw::EntryRow::new(); + let smart_summary_prompt_prefix = adw::EntryRow::new(); + let smart_summary_prompt_suffix = adw::EntryRow::new(); + smart_summary_group.add(&smart_summary_model); + smart_summary_group.add(&smart_summary_system_prompt); + smart_summary_group.add(&smart_summary_prompt_prefix); + smart_summary_group.add(&smart_summary_prompt_suffix); + page.add(&smart_summary_group); + + let widgets = Self { + page, + + general_group, + llm_endpoint, + + smart_summary_group, + smart_summary_model, + smart_summary_system_prompt, + smart_summary_prompt_prefix, + smart_summary_prompt_suffix + }; + + let schema = settings.settings_schema().unwrap(); + + for (row, key) in [ + (&widgets.llm_endpoint, "llm-endpoint"), + (&widgets.smart_summary_model, "smart-summary-model"), + (&widgets.smart_summary_system_prompt, "smart-summary-system-prompt"), + (&widgets.smart_summary_prompt_prefix, "smart-summary-prompt-prefix"), + (&widgets.smart_summary_prompt_suffix, "smart-summary-prompt-suffix"), + ] { + settings.bind(key, row, "text") + .get() + .set() + .build(); + row.set_title(&gettext(schema.key(key).summary().unwrap())); + } + + widgets + } +} + +pub struct PreferencesWidgets { + #[cfg(feature = "smart-summary")] + llm: LanguageModelPreferencesWidgets +} + impl Component for Preferences { type CommandOutput = (); type Input = Option<gtk::Widget>; type Output = (); type Init = (); + type Root = adw::PreferencesDialog; + type Widgets = PreferencesWidgets; - view! { - #[root] - adw::PreferencesDialog { - add = &adw::PreferencesPage { - set_title: &gettext("Language Models"), - set_description: &gettext("Settings for the language model integrations."), - set_icon_name: Some("magic-wand"), - - adw::PreferencesGroup { - set_title: &gettext("General"), - - #[name = "llm_endpoint"] - adw::EntryRow {}, - }, - - adw::PreferencesGroup { - set_title: &gettext("Smart Summary"), - - #[name = "smart_summary_model"] adw::EntryRow {}, - #[name = "smart_summary_system_prompt"] adw::EntryRow {}, - #[name = "smart_summary_prompt_prefix"] adw::EntryRow {}, - #[name = "smart_summary_prompt_suffix"] adw::EntryRow {}, - } - } - } + fn init_root() -> Self::Root { + adw::PreferencesDialog::new() } fn init( @@ -51,23 +112,13 @@ impl Component for Preferences { }; model.settings.delay(); - let schema = model.settings.settings_schema().unwrap(); - let widgets = view_output!(); - - for (row, key) in [ - (&widgets.llm_endpoint, "llm-endpoint"), - (&widgets.smart_summary_model, "smart-summary-model"), - (&widgets.smart_summary_system_prompt, "smart-summary-system-prompt"), - (&widgets.smart_summary_prompt_prefix, "smart-summary-prompt-prefix"), - (&widgets.smart_summary_prompt_suffix, "smart-summary-prompt-suffix"), - ] { - model.settings.bind(key, row, "text") - .get() - .set() - .build(); - row.set_title(&gettext(schema.key(key).summary().unwrap())); - } + let widgets = PreferencesWidgets { + #[cfg(feature = "smart-summary")] + llm: LanguageModelPreferencesWidgets::new(&model.settings), + }; + #[cfg(feature = "smart-summary")] + root.add(&widgets.llm.page); root.connect_closed(glib::clone!( #[strong(rename_to = settings)] diff --git a/src/components/smart_summary.rs b/src/components/smart_summary.rs index 7b2df7d..2795b09 100644 --- a/src/components/smart_summary.rs +++ b/src/components/smart_summary.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "smart-summary")] use futures::AsyncBufReadExt; use gio::prelude::SettingsExtManual; use soup::prelude::*; diff --git a/src/lib.rs b/src/lib.rs index 6421560..37b54a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,9 @@ use libsecret::prelude::{RetrievableExtManual, RetrievableExt}; use relm4::{actions::{RelmAction, RelmActionGroup}, gtk, loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt}; pub mod components { + #[cfg(feature = "smart-summary")] pub(crate) mod smart_summary; + #[cfg(feature = "smart-summary")] pub(crate) use smart_summary::{ SmartSummaryButton, Output as SmartSummaryOutput, Input as SmartSummaryInput }; @@ -257,9 +259,16 @@ impl AsyncComponent for App { micropub: state, secret_schema, - post_editor: components::PostEditor::builder() - .launch((http.clone(), None)) - .forward(sender.input_sender(), Self::Input::PostEditor), + post_editor: { + #[cfg(feature = "smart-summary")] + let init = (http.clone(), None); + #[cfg(not(feature = "smart-summary"))] + let init = None; + + components::PostEditor::builder() + .launch(init) + .forward(sender.input_sender(), Self::Input::PostEditor) + }, signin: components::SignIn::builder() .launch((glib::Uri::parse( CLIENT_ID_STR, glib::UriFlags::NONE diff --git a/src/meson.build b/src/meson.build index 350b911..3bc0c3f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -10,6 +10,12 @@ else message('Building in debug mode') endif +cargo_options += [ '--no-default-features' ] + +if get_option('llm') + cargo_options += [ '--features=smart-summary' ] +endif + cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home', 'PKGDATADIR=' + pkgdatadir, |