diff --git a/gui/Cargo.lock b/gui/Cargo.lock index c353e4cb..4b889c27 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -2431,7 +2431,7 @@ dependencies = [ [[package]] name = "liana" version = "4.0.0" -source = "git+https://github.com/wizardsardine/liana?branch=master#dee069e72343a67607f4429125e4c80dc0d73055" +source = "git+https://github.com/wizardsardine/liana?branch=master#b8f8d1b944120879986a71255bab19e5b342ecc8" dependencies = [ "backtrace", "bdk_coin_select", diff --git a/gui/src/app/state/settings/bitcoind.rs b/gui/src/app/state/settings/bitcoind.rs index e4e11cf2..966c6a1a 100644 --- a/gui/src/app/state/settings/bitcoind.rs +++ b/gui/src/app/state/settings/bitcoind.rs @@ -8,12 +8,13 @@ use chrono::prelude::*; use iced::Command; use tracing::info; -use liana::config::{BitcoinConfig, BitcoindConfig, Config}; +use liana::config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config}; use liana_ui::{component::form, widget::Element}; use crate::{ app::{cache::Cache, error::Error, message::Message, state::settings::Setting, view, State}, + bitcoind::{RpcAuthType, RpcAuthValues}, daemon::Daemon, }; @@ -145,7 +146,8 @@ pub struct BitcoindSettings { bitcoin_config: BitcoinConfig, edit: bool, processing: bool, - cookie_path: form::Value, + rpc_auth_vals: RpcAuthValues, + selected_auth_type: RpcAuthType, addr: form::Value, daemon_is_external: bool, bitcoind_is_internal: bool, @@ -164,7 +166,33 @@ impl BitcoindSettings { daemon_is_external: bool, bitcoind_is_internal: bool, ) -> BitcoindSettings { - let path = bitcoind_config.cookie_path.to_str().unwrap().to_string(); + let (rpc_auth_vals, selected_auth_type) = match &bitcoind_config.rpc_auth { + BitcoindRpcAuth::CookieFile(path) => ( + RpcAuthValues { + cookie_path: form::Value { + valid: true, + value: path.to_str().unwrap().to_string(), + }, + user: form::Value::default(), + password: form::Value::default(), + }, + RpcAuthType::CookieFile, + ), + BitcoindRpcAuth::UserPass(user, password) => ( + RpcAuthValues { + cookie_path: form::Value::default(), + user: form::Value { + valid: true, + value: user.clone(), + }, + password: form::Value { + valid: true, + value: password.clone(), + }, + }, + RpcAuthType::UserPass, + ), + }; let addr = bitcoind_config.addr.to_string(); BitcoindSettings { daemon_is_external, @@ -173,10 +201,8 @@ impl BitcoindSettings { bitcoin_config, edit: false, processing: false, - cookie_path: form::Value { - valid: true, - value: path, - }, + rpc_auth_vals, + selected_auth_type, addr: form::Value { valid: true, value: addr, @@ -214,21 +240,41 @@ impl Setting for BitcoindSettings { if !self.processing { match field { "socket_address" => self.addr.value = value, - "cookie_file_path" => self.cookie_path.value = value, + "cookie_file_path" => self.rpc_auth_vals.cookie_path.value = value, + "user" => self.rpc_auth_vals.user.value = value, + "password" => self.rpc_auth_vals.password.value = value, _ => {} } } } + view::SettingsEditMessage::BitcoindRpcAuthTypeSelected(auth_type) => { + if !self.processing { + self.selected_auth_type = auth_type; + } + } view::SettingsEditMessage::Confirm => { let new_addr = SocketAddr::from_str(&self.addr.value); self.addr.valid = new_addr.is_ok(); - let new_path = PathBuf::from_str(&self.cookie_path.value); - self.cookie_path.valid = new_path.is_ok(); + let rpc_auth = match self.selected_auth_type { + RpcAuthType::CookieFile => { + let new_path = PathBuf::from_str(&self.rpc_auth_vals.cookie_path.value); + if let Ok(path) = new_path { + self.rpc_auth_vals.cookie_path.valid = true; + Some(BitcoindRpcAuth::CookieFile(path)) + } else { + None + } + } + RpcAuthType::UserPass => Some(BitcoindRpcAuth::UserPass( + self.rpc_auth_vals.user.value.clone(), + self.rpc_auth_vals.password.value.clone(), + )), + }; - if self.addr.valid & self.cookie_path.valid { + if let (true, Some(rpc_auth)) = (self.addr.valid, rpc_auth) { let mut daemon_config = daemon.config().cloned().unwrap(); daemon_config.bitcoind_config = Some(liana::config::BitcoindConfig { - cookie_path: new_path.unwrap(), + rpc_auth, addr: new_addr.unwrap(), }); self.processing = true; @@ -247,7 +293,8 @@ impl Setting for BitcoindSettings { self.bitcoin_config.network, cache.blockheight, &self.addr, - &self.cookie_path, + &self.rpc_auth_vals, + &self.selected_auth_type, self.processing, ) } else { @@ -347,6 +394,7 @@ impl Setting for RescanSetting { Message::StartRescan, ); } + _ => {} }; Command::none() } diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index 919441d2..44cd6043 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -1,4 +1,4 @@ -use crate::app::menu::Menu; +use crate::{app::menu::Menu, bitcoind::RpcAuthType}; use liana::miniscript::bitcoin::bip32::Fingerprint; #[derive(Debug, Clone)] @@ -75,6 +75,7 @@ pub enum SettingsMessage { pub enum SettingsEditMessage { Select, FieldEdited(&'static str, String), + BitcoindRpcAuthTypeSelected(RpcAuthType), Cancel, Confirm, } diff --git a/gui/src/app/view/settings.rs b/gui/src/app/view/settings.rs index 2b896b15..4618ad4b 100644 --- a/gui/src/app/view/settings.rs +++ b/gui/src/app/view/settings.rs @@ -3,11 +3,14 @@ use std::str::FromStr; use iced::{ alignment, - widget::{scrollable, Space}, + widget::{radio, scrollable, Space}, Alignment, Length, }; -use liana::miniscript::bitcoin::{bip32::Fingerprint, Network}; +use liana::{ + config::BitcoindRpcAuth, + miniscript::bitcoin::{bip32::Fingerprint, Network}, +}; use super::{dashboard, message::*}; @@ -26,6 +29,7 @@ use crate::{ menu::Menu, view::{hw, warning::warn}, }, + bitcoind::{RpcAuthType, RpcAuthValues}, hw::HardwareWallet, }; @@ -208,7 +212,8 @@ pub fn bitcoind_edit<'a>( network: Network, blockheight: i32, addr: &form::Value, - cookie_path: &form::Value, + rpc_auth_vals: &RpcAuthValues, + selected_auth_type: &RpcAuthType, processing: bool, ) -> Element<'a, SettingsEditMessage> { let mut col = Column::new().spacing(20); @@ -244,18 +249,60 @@ pub fn bitcoind_edit<'a>( col = col .push( - Column::new() - .push(text("Cookie file path:").bold().small()) + [RpcAuthType::CookieFile, RpcAuthType::UserPass] + .iter() + .fold( + Row::new() + .push(text("RPC authentication:").small().bold()) + .spacing(10), + |row, auth_type| { + row.push(radio( + format!("{}", auth_type), + *auth_type, + Some(*selected_auth_type), + SettingsEditMessage::BitcoindRpcAuthTypeSelected, + )) + .spacing(30) + .align_items(Alignment::Center) + }, + ), + ) + .push(match selected_auth_type { + RpcAuthType::CookieFile => Column::new() .push( - form::Form::new_trimmed("Cookie file path", cookie_path, |value| { - SettingsEditMessage::FieldEdited("cookie_file_path", value) - }) + form::Form::new_trimmed( + "Cookie file path", + &rpc_auth_vals.cookie_path, + |value| SettingsEditMessage::FieldEdited("cookie_file_path", value), + ) .warning("Please enter a valid filesystem path") .size(20) .padding(5), ) .spacing(5), - ) + RpcAuthType::UserPass => Column::new() + .push( + Row::new() + .push( + form::Form::new_trimmed("User", &rpc_auth_vals.user, |value| { + SettingsEditMessage::FieldEdited("user", value) + }) + .warning("Please enter a valid user") + .size(20) + .padding(5), + ) + .push( + form::Form::new_trimmed("Password", &rpc_auth_vals.password, |value| { + SettingsEditMessage::FieldEdited("password", value) + }) + .warning("Please enter a valid password") + .size(20) + .padding(5), + ) + .spacing(10), + ) + .spacing(5), + }) .push( Column::new() .push(text("Socket address:").bold().small()) @@ -345,13 +392,17 @@ pub fn bitcoind<'a>( .push(separation().width(Length::Fill)); } - let rows = vec![ - ( - "Cookie file path:", - config.cookie_path.to_str().unwrap().to_string(), - ), - ("Socket address:", config.addr.to_string()), - ]; + let mut rows = vec![]; + match &config.rpc_auth { + BitcoindRpcAuth::CookieFile(path) => { + rows.push(("Cookie file path:", path.to_str().unwrap().to_string())); + } + BitcoindRpcAuth::UserPass(user, password) => { + rows.push(("User:", user.clone())); + rows.push(("Password:", password.clone())); + } + } + rows.push(("Socket address:", config.addr.to_string())); let mut col_fields = Column::new(); for (k, v) in rows { diff --git a/gui/src/bitcoind.rs b/gui/src/bitcoind.rs index f6a3f0d5..69aaa5dc 100644 --- a/gui/src/bitcoind.rs +++ b/gui/src/bitcoind.rs @@ -1,7 +1,9 @@ use liana::{ - config::BitcoindConfig, + config::{BitcoindConfig, BitcoindRpcAuth}, miniscript::bitcoin::{self, Network}, }; +use liana_ui::component::form; +use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::thread; @@ -222,6 +224,7 @@ impl Bitcoind { // We've started bitcoind in the background, however it may fail to start for whatever // reason. And we need its JSONRPC interface to be available to continue. Thus wait for it // to have created the cookie file, regularly checking it did not fail to start. + let cookie_path = internal_bitcoind_cookie_path(&bitcoind_datadir, network); loop { match process.try_wait() { Ok(None) => {} @@ -229,11 +232,11 @@ impl Bitcoind { Ok(Some(status)) => { log::error!("Bitcoind exited with status '{}'", status); return Err(StartInternalBitcoindError::CookieFileNotFound( - config.cookie_path.to_string_lossy().into_owned(), + cookie_path.to_string_lossy().into_owned(), )); } } - if config.cookie_path.exists() { + if cookie_path.exists() { log::info!("Bitcoind seems to have successfully started."); break; } @@ -241,9 +244,10 @@ impl Bitcoind { thread::sleep(time::Duration::from_millis(500)); } - config.cookie_path = config.cookie_path.canonicalize().map_err(|e| { + config.rpc_auth = BitcoindRpcAuth::CookieFile(cookie_path.canonicalize().map_err(|e| { StartInternalBitcoindError::CouldNotCanonicalizeCookiePath(e.to_string()) - })?; + })?); + liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string()) .map_err(|e| StartInternalBitcoindError::BitcoinDError(e.to_string()))?; @@ -273,3 +277,44 @@ pub fn stop_bitcoind(config: &BitcoindConfig) -> bool { } } } + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum RpcAuthType { + CookieFile, + UserPass, +} + +impl fmt::Display for RpcAuthType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RpcAuthType::CookieFile => write!(f, "Cookie file path"), + RpcAuthType::UserPass => write!(f, "User and password"), + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct RpcAuthValues { + pub cookie_path: form::Value, + pub user: form::Value, + pub password: form::Value, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ConfigField { + Address, + CookieFilePath, + User, + Password, +} + +impl fmt::Display for ConfigField { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ConfigField::Address => write!(f, "Socket address"), + ConfigField::CookieFilePath => write!(f, "Cookie file path"), + ConfigField::User => write!(f, "User"), + ConfigField::Password => write!(f, "Password"), + } + } +} diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 66cb21e7..d6e8afad 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -5,7 +5,11 @@ use liana::miniscript::{ use std::path::PathBuf; use super::Error; -use crate::{bitcoind::Bitcoind, download::Progress, hw::HardwareWalletMessage}; +use crate::{ + bitcoind::{Bitcoind, ConfigField, RpcAuthType}, + download::Progress, + hw::HardwareWalletMessage, +}; use async_hwi::DeviceKind; #[derive(Debug, Clone)] @@ -39,8 +43,8 @@ pub enum Message { #[derive(Debug, Clone)] pub enum DefineBitcoind { - CookiePathEdited(String), - AddressEdited(String), + ConfigFieldEdited(ConfigField, String), + RpcAuthTypeSelected(RpcAuthType), PingBitcoindResult(Result<(), Error>), PingBitcoind, } diff --git a/gui/src/installer/step/bitcoind.rs b/gui/src/installer/step/bitcoind.rs index 544cb415..6572d36d 100644 --- a/gui/src/installer/step/bitcoind.rs +++ b/gui/src/installer/step/bitcoind.rs @@ -9,7 +9,10 @@ use bitcoin_hashes::{sha256, Hash}; #[cfg(any(target_os = "macos", target_os = "linux"))] use flate2::read::GzDecoder; use iced::{Command, Subscription}; -use liana::{config::BitcoindConfig, miniscript::bitcoin::Network}; +use liana::{ + config::{BitcoindConfig, BitcoindRpcAuth}, + miniscript::bitcoin::Network, +}; #[cfg(any(target_os = "macos", target_os = "linux"))] use tar::Archive; use tracing::info; @@ -21,7 +24,7 @@ use liana_ui::{component::form, widget::*}; use crate::{ bitcoind::{ self, bitcoind_network_dir, internal_bitcoind_datadir, internal_bitcoind_directory, - Bitcoind, StartInternalBitcoindError, VERSION, + Bitcoind, ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError, VERSION, }, download, hw::HardwareWallets, @@ -465,7 +468,8 @@ impl Step for SelectBitcoindTypeStep { } pub struct DefineBitcoind { - cookie_path: form::Value, + rpc_auth_vals: RpcAuthValues, + selected_auth_type: RpcAuthType, address: form::Value, is_running: Option>, } @@ -473,7 +477,8 @@ pub struct DefineBitcoind { impl DefineBitcoind { pub fn new() -> Self { Self { - cookie_path: form::Value::default(), + rpc_auth_vals: RpcAuthValues::default(), + selected_auth_type: RpcAuthType::CookieFile, address: form::Value::default(), is_running: None, } @@ -481,16 +486,28 @@ impl DefineBitcoind { pub fn ping(&self) -> Command { let address = self.address.value.to_owned(); - let cookie_path = self.cookie_path.value.to_owned(); + let selected_auth_type = self.selected_auth_type; + let rpc_auth_vals = self.rpc_auth_vals.clone(); Command::perform( async move { - let cookie = std::fs::read_to_string(&cookie_path) - .map_err(|e| Error::Bitcoind(format!("Failed to read cookie file: {}", e)))?; + let builder = match selected_auth_type { + RpcAuthType::CookieFile => { + let cookie_path = rpc_auth_vals.cookie_path.value; + let cookie = std::fs::read_to_string(&cookie_path).map_err(|e| { + Error::Bitcoind(format!("Failed to read cookie file: {}", e)) + })?; + SimpleHttpTransport::builder().cookie_auth(cookie) + } + RpcAuthType::UserPass => { + let user = rpc_auth_vals.user.value; + let password = rpc_auth_vals.password.value; + SimpleHttpTransport::builder().auth(user, Some(password)) + } + }; let client = Client::with_transport( - SimpleHttpTransport::builder() + builder .url(&address)? .timeout(std::time::Duration::from_secs(3)) - .cookie_auth(cookie) .build(), ); client.send_request(client.build_request("echo", &[]))?; @@ -503,8 +520,8 @@ impl DefineBitcoind { impl Step for DefineBitcoind { fn load_context(&mut self, ctx: &Context) { - if self.cookie_path.value.is_empty() { - self.cookie_path.value = + if self.rpc_auth_vals.cookie_path.value.is_empty() { + self.rpc_auth_vals.cookie_path.value = bitcoind_default_cookie_path(&ctx.bitcoin_config.network).unwrap_or_default() } if self.address.value.is_empty() { @@ -519,15 +536,31 @@ impl Step for DefineBitcoind { return self.ping(); } message::DefineBitcoind::PingBitcoindResult(res) => self.is_running = Some(res), - message::DefineBitcoind::AddressEdited(address) => { + message::DefineBitcoind::ConfigFieldEdited(field, value) => match field { + ConfigField::Address => { + self.is_running = None; + self.address.value = value; + self.address.valid = true; + } + ConfigField::CookieFilePath => { + self.is_running = None; + self.rpc_auth_vals.cookie_path.value = value; + self.rpc_auth_vals.cookie_path.valid = true; + } + ConfigField::User => { + self.is_running = None; + self.rpc_auth_vals.user.value = value; + self.rpc_auth_vals.user.valid = true; + } + ConfigField::Password => { + self.is_running = None; + self.rpc_auth_vals.password.value = value; + self.rpc_auth_vals.password.valid = true; + } + }, + message::DefineBitcoind::RpcAuthTypeSelected(auth_type) => { self.is_running = None; - self.address.value = address; - self.address.valid = true; - } - message::DefineBitcoind::CookiePathEdited(path) => { - self.is_running = None; - self.cookie_path.value = path; - self.address.valid = true; + self.selected_auth_type = auth_type; } }; }; @@ -535,28 +568,29 @@ impl Step for DefineBitcoind { } fn apply(&mut self, ctx: &mut Context) -> bool { - match ( - PathBuf::from_str(&self.cookie_path.value), - std::net::SocketAddr::from_str(&self.address.value), - ) { - (Err(_), Ok(_)) => { - self.cookie_path.valid = false; - false + let addr = std::net::SocketAddr::from_str(&self.address.value); + let rpc_auth = match self.selected_auth_type { + RpcAuthType::CookieFile => { + if let Ok(path) = PathBuf::from_str(&self.rpc_auth_vals.cookie_path.value) { + Some(BitcoindRpcAuth::CookieFile(path)) + } else { + self.rpc_auth_vals.cookie_path.valid = false; + None + } } - (Ok(_), Err(_)) => { + RpcAuthType::UserPass => Some(BitcoindRpcAuth::UserPass( + self.rpc_auth_vals.user.value.clone(), + self.rpc_auth_vals.password.value.clone(), + )), + }; + match (rpc_auth, addr) { + (None, Ok(_)) => false, + (_, Err(_)) => { self.address.valid = false; false } - (Err(_), Err(_)) => { - self.cookie_path.valid = false; - self.address.valid = false; - false - } - (Ok(path), Ok(addr)) => { - ctx.bitcoind_config = Some(BitcoindConfig { - cookie_path: path, - addr, - }); + (Some(rpc_auth), Ok(addr)) => { + ctx.bitcoind_config = Some(BitcoindConfig { rpc_auth, addr }); true } } @@ -566,7 +600,8 @@ impl Step for DefineBitcoind { view::define_bitcoin( progress, &self.address, - &self.cookie_path, + &self.rpc_auth_vals, + &self.selected_auth_type, self.is_running.as_ref(), ) } @@ -803,7 +838,7 @@ impl Step for InternalBitcoindStep { match Bitcoind::start( &self.network, BitcoindConfig { - cookie_path, + rpc_auth: BitcoindRpcAuth::CookieFile(cookie_path), addr: internal_bitcoind_address(rpc_port), }, &self.liana_datadir, diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 02e78d8c..6ed49ad7 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -1,5 +1,6 @@ use iced::widget::{ - checkbox, container, pick_list, scrollable, scrollable::Properties, slider, Space, TextInput, + checkbox, container, pick_list, radio, scrollable, scrollable::Properties, slider, Space, + TextInput, }; use iced::{alignment, widget::progress_bar, Alignment, Length}; @@ -21,7 +22,7 @@ use liana_ui::{ }; use crate::{ - bitcoind::StartInternalBitcoindError, + bitcoind::{ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError}, hw::HardwareWallet, installer::{ message::{self, Message}, @@ -787,14 +788,18 @@ pub fn help_backup<'a>() -> Element<'a, Message> { pub fn define_bitcoin<'a>( progress: (usize, usize), address: &form::Value, - cookie_path: &form::Value, + rpc_auth_vals: &RpcAuthValues, + selected_auth_type: &RpcAuthType, is_running: Option<&Result<(), Error>>, ) -> Element<'a, Message> { let col_address = Column::new() .push(text("Address:").bold()) .push( form::Form::new_trimmed("Address", address, |msg| { - Message::DefineBitcoind(message::DefineBitcoind::AddressEdited(msg)) + Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited( + ConfigField::Address, + msg, + )) }) .warning("Please enter correct address") .size(20) @@ -802,16 +807,67 @@ pub fn define_bitcoin<'a>( ) .spacing(10); - let col_cookie = Column::new() - .push(text("Cookie path:").bold()) + let col_auth = Column::new() .push( - form::Form::new_trimmed("Cookie path", cookie_path, |msg| { - Message::DefineBitcoind(message::DefineBitcoind::CookiePathEdited(msg)) - }) - .warning("Please enter correct path") - .size(20) - .padding(10), + [RpcAuthType::CookieFile, RpcAuthType::UserPass] + .iter() + .fold( + Row::new() + .push(text("RPC authentication:").small().bold()) + .spacing(10), + |row, auth_type| { + row.push(radio( + format!("{}", auth_type), + *auth_type, + Some(*selected_auth_type), + |new_selection| { + Message::DefineBitcoind( + message::DefineBitcoind::RpcAuthTypeSelected(new_selection), + ) + }, + )) + .spacing(30) + .align_items(Alignment::Center) + }, + ), ) + .push(match selected_auth_type { + RpcAuthType::CookieFile => Row::new().push( + form::Form::new_trimmed("Cookie path", &rpc_auth_vals.cookie_path, |msg| { + Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited( + ConfigField::CookieFilePath, + msg, + )) + }) + .warning("Please enter correct path") + .size(20) + .padding(10), + ), + RpcAuthType::UserPass => Row::new() + .push( + form::Form::new_trimmed("User", &rpc_auth_vals.user, |msg| { + Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited( + ConfigField::User, + msg, + )) + }) + .warning("Please enter correct user") + .size(20) + .padding(10), + ) + .push( + form::Form::new_trimmed("Password", &rpc_auth_vals.password, |msg| { + Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited( + ConfigField::Password, + msg, + )) + }) + .warning("Please enter correct password") + .size(20) + .padding(10), + ) + .spacing(10), + }) .spacing(10); layout( @@ -819,7 +875,7 @@ pub fn define_bitcoin<'a>( "Set up connection to the Bitcoin full node", Column::new() .push(col_address) - .push(col_cookie) + .push(col_auth) .push_maybe(if is_running.is_some() { is_running.map(|res| { if res.is_ok() { diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs index 20fb3380..777ba1a0 100644 --- a/gui/ui/src/theme.rs +++ b/gui/ui/src/theme.rs @@ -345,7 +345,7 @@ impl radio::StyleSheet for Theme { background: iced::Color::TRANSPARENT.into(), dot_color: color::GREEN, border_width: 1.0, - border_color: color::GREEN, + border_color: color::GREY_7, text_color: None, } } @@ -354,6 +354,7 @@ impl radio::StyleSheet for Theme { let active = self.active(style, is_selected); radio::Appearance { dot_color: color::GREEN, + border_color: color::GREEN, background: iced::Color::TRANSPARENT.into(), ..active }