diff --git a/src/parse/config.rs b/src/parse/config.rs index 9ed5344..8582e82 100644 --- a/src/parse/config.rs +++ b/src/parse/config.rs @@ -1,6 +1,12 @@ -use std::{error::Error, env, path::Path, fs::{self, File}, io::Write}; +use std::{ + env, + error::Error, + fs::{self, File}, + io::Write, + path::Path, +}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub struct NscConfig { @@ -8,20 +14,18 @@ pub struct NscConfig { pub flake: Option, } -pub fn getconfig() -> NscConfig { +pub fn getconfig() -> Option { if let Ok(c) = getconfigval() { - c + Some(c) } else { - NscConfig { - systemconfig: String::from("/etc/nixos/configuration.nix"), - flake: None, - } + None } } fn getconfigval() -> Result> { let configfile = checkconfig()?; - let config: NscConfig = serde_json::from_reader(File::open(format!("{}/config.json", configfile))?)?; + let config: NscConfig = + serde_json::from_reader(File::open(format!("{}/config.json", configfile))?)?; Ok(config) } @@ -29,8 +33,10 @@ fn checkconfig() -> Result> { let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?); if !Path::is_file(Path::new(&format!("{}/config.json", &cfgdir))) { if !Path::is_file(Path::new("/etc/nix-software-center/config.json")) { - createdefaultconfig()?; - Ok(cfgdir) + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "No config file found", + ))) } else { Ok("/etc/nix-software-center/".to_string()) } @@ -39,19 +45,6 @@ fn checkconfig() -> Result> { } } -// fn configexists() -> Result> { -// let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?); -// if !Path::is_file(Path::new(&format!("{}/config.json", &cfgdir))) { -// if !Path::is_file(Path::new("/etc/nix-software-center/config.json")) { -// Ok(false) -// } else { -// Ok(true) -// } -// } else { -// Ok(true) -// } -// } - pub fn editconfig(config: NscConfig) -> Result<(), Box> { let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?); fs::create_dir_all(&cfgdir)?; @@ -59,40 +52,4 @@ pub fn editconfig(config: NscConfig) -> Result<(), Box> { let mut file = File::create(format!("{}/config.json", cfgdir))?; file.write_all(json.as_bytes())?; Ok(()) -} - -fn createdefaultconfig() -> Result<(), Box> { - let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?); - fs::create_dir_all(&cfgdir)?; - let config = NscConfig { - systemconfig: "/etc/nixos/configuration.nix".to_string(), - flake: None, - }; - let json = serde_json::to_string_pretty(&config)?; - let mut file = File::create(format!("{}/config.json", cfgdir))?; - file.write_all(json.as_bytes())?; - Ok(()) -} - - -// pub fn readconfig(cfg: String) -> Result<(String, Option), Box> { -// let file = fs::read_to_string(cfg)?; -// let config: NscConfig = match serde_json::from_str(&file) { -// Ok(x) => x, -// Err(_) => { -// createdefaultconfig()?; -// return Ok(( -// "/etc/nixos/configuration.nix".to_string(), -// None, -// )); -// } -// }; -// if Path::is_file(Path::new(&config.systemconfig)) { -// Ok((config.systemconfig, config.flake)) -// } else { -// Ok(( -// "/etc/nixos/configuration.nix".to_string(), -// None, -// )) -// } -// } +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4190d00..d2b9873 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -14,3 +14,4 @@ pub mod about; pub mod preferencespage; pub mod categorypage; pub mod categorytile; +pub mod welcome; diff --git a/src/ui/pkgpage.rs b/src/ui/pkgpage.rs index 7c7d6d3..7e9df3b 100644 --- a/src/ui/pkgpage.rs +++ b/src/ui/pkgpage.rs @@ -23,7 +23,6 @@ use std::{ use log::*; use crate::parse::config::NscConfig; -use crate::parse::config::getconfig; use crate::parse::packages::PkgMaintainer; use crate::parse::packages::StrOrVec; use crate::ui::installworker::InstallAsyncHandlerMsg; @@ -169,14 +168,15 @@ pub enum PkgAsyncMsg { } #[derive(Debug)] -pub struct PkgPageTypes { +pub struct PkgPageInit { pub syspkgs: SystemPkgs, - pub userpkgs: UserPkgs + pub userpkgs: UserPkgs, + pub config: NscConfig, } #[relm4::component(pub)] impl Component for PkgModel { - type Init = PkgPageTypes; + type Init = PkgPageInit; type Input = PkgMsg; type Output = AppMsg; type Widgets = PkgWidgets; @@ -932,14 +932,14 @@ impl Component for PkgModel { } fn init( - pkgtypes: Self::Init, + initparams: Self::Init, root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { let installworker = InstallAsyncHandler::builder() - .detach_worker(InstallAsyncHandlerInit { syspkgs: pkgtypes.syspkgs.clone(), userpkgs: pkgtypes.userpkgs.clone() }) + .detach_worker(InstallAsyncHandlerInit { syspkgs: initparams.syspkgs.clone(), userpkgs: initparams.userpkgs.clone() }) .forward(sender.input_sender(), identity); - let config = getconfig(); + let config = initparams.config; installworker.emit(InstallAsyncHandlerMsg::SetConfig(config.clone())); let model = PkgModel { config, @@ -962,8 +962,8 @@ impl Component for PkgModel { // installinguserpkgs: HashSet::new(), // installingsystempkgs: HashSet::new(), // removinguserpkgs: HashSet::new(), - syspkgtype: pkgtypes.syspkgs, - userpkgtype: pkgtypes.userpkgs, + syspkgtype: initparams.syspkgs, + userpkgtype: initparams.userpkgs, workqueue: HashSet::new(), launchable: None, tracker: 0, diff --git a/src/ui/updatepage.rs b/src/ui/updatepage.rs index 47e34aa..0e31909 100644 --- a/src/ui/updatepage.rs +++ b/src/ui/updatepage.rs @@ -1,4 +1,4 @@ -use crate::{parse::{cache::channelver, config::{getconfig, NscConfig}}, APPINFO}; +use crate::{parse::{cache::channelver, config::NscConfig}, APPINFO}; use super::{pkgpage::InstallType, window::*, updatedialog::{UpdateDialogModel, UpdateDialogMsg}, updateworker::{UpdateAsyncHandler, UpdateAsyncHandlerMsg, UpdateAsyncHandlerInit}}; use adw::prelude::*; @@ -44,6 +44,7 @@ pub struct UpdatePageInit { pub window: gtk::Window, pub systype: SystemPkgs, pub usertype: UserPkgs, + pub config: NscConfig, } #[relm4::component(pub)] @@ -286,7 +287,7 @@ impl SimpleComponent for UpdatePageModel { .detach_worker(UpdateAsyncHandlerInit { syspkgs: initparams.systype.clone(), userpkgs: initparams.usertype.clone() }) .forward(sender.input_sender(), identity); - let config = getconfig(); + let config = initparams.config; updateworker.emit(UpdateAsyncHandlerMsg::UpdateConfig(config.clone())); let model = UpdatePageModel { diff --git a/src/ui/welcome.rs b/src/ui/welcome.rs new file mode 100644 index 0000000..ff4d619 --- /dev/null +++ b/src/ui/welcome.rs @@ -0,0 +1,250 @@ +use std::path::{PathBuf, Path}; + +use adw::prelude::*; +use log::info; +use relm4::*; +use relm4_components::open_dialog::*; + +use crate::parse::config::NscConfig; + +use super::window::AppMsg; + +#[tracker::track] +pub struct WelcomeModel { + hidden: bool, + confpath: Option, + flake: bool, + flakepath: Option, + #[tracker::no_eq] + conf_dialog: Controller, + #[tracker::no_eq] + flake_dialog: Controller, +} + +#[derive(Debug)] +pub enum WelcomeMsg { + Show, + Close, + UpdateConfPath(PathBuf), + UpdateFlakePath(PathBuf), + ClearFlakePath, + OpenConf, + OpenFlake, + Ignore, +} + +#[relm4::component(pub)] +impl SimpleComponent for WelcomeModel { + type InitParams = gtk::Window; + type Input = WelcomeMsg; + type Output = AppMsg; + type Widgets = WelcomeWidgets; + + view! { + window = adw::Window { + set_transient_for: Some(&parent_window), + set_modal: true, + #[watch] + set_visible: !model.hidden, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + gtk::Box { + set_valign: gtk::Align::Center, + set_vexpand: true, + set_orientation: gtk::Orientation::Vertical, + set_spacing: 20, + set_margin_all: 20, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 10, + gtk::Label { + add_css_class: "title-1", + set_text: "Welcome the Nix Software Center!", + set_justify: gtk::Justification::Center, + }, + gtk::Label { + add_css_class: "dim-label", + set_text: "If your configuration file is not in the default location, you can change it here.", + }, + }, + gtk::ListBox { + add_css_class: "boxed-list", + set_halign: gtk::Align::Fill, + set_selection_mode: gtk::SelectionMode::None, + adw::ActionRow { + set_title: "Configuration file", + add_suffix = >k::Button { + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + gtk::Image { + set_icon_name: Some("document-open-symbolic"), + }, + gtk::Label { + #[watch] + set_label: { + if let Some(path) = &model.confpath { + let x = path.file_name().unwrap_or_default().to_str().unwrap_or_default(); + if x.is_empty() { + "(None)" + } else { + x + } + } else { + "(None)" + } + } + } + }, + connect_clicked[sender] => move |_| { + sender.input(WelcomeMsg::OpenConf); + } + }, + }, + }, + gtk::ListBox { + add_css_class: "boxed-list", + set_halign: gtk::Align::Fill, + set_selection_mode: gtk::SelectionMode::None, + adw::ActionRow { + set_title: "Flake file", + set_subtitle: "If you are using flakes, you can specify the path to your flake.nix file here.", + add_suffix = >k::Button { + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + gtk::Image { + set_icon_name: Some("document-open-symbolic"), + }, + gtk::Label { + #[watch] + set_label: { + if let Some(path) = &model.flakepath { + let x = path.file_name().unwrap_or_default().to_str().unwrap_or_default(); + if x.is_empty() { + "(None)" + } else { + x + } + } else { + "(None)" + } + } + } + }, + connect_clicked[sender] => move |_| { + sender.input(WelcomeMsg::OpenFlake); + } + }, + add_suffix = >k::Button { + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + set_icon_name: "user-trash-symbolic", + connect_clicked[sender] => move |_| { + sender.input(WelcomeMsg::ClearFlakePath); + } + } + }, + }, + #[name(btn)] + gtk::Button { + #[watch] + set_sensitive: model.confpath.is_some(), + add_css_class: "pill", + add_css_class: "suggested-action", + set_label: "Continue", + set_hexpand: false, + set_halign: gtk::Align::Center, + connect_clicked[sender] => move |_| { + sender.input(WelcomeMsg::Close); + }, + } + } + } + } + } + + fn init( + parent_window: Self::InitParams, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + + let conf_dialog = OpenDialog::builder() + .transient_for_native(root) + .launch(OpenDialogSettings::default()) + .forward(&sender.input, |response| match response { + OpenDialogResponse::Accept(path) => WelcomeMsg::UpdateConfPath(path), + OpenDialogResponse::Cancel => WelcomeMsg::Ignore, + }); + + + let flake_dialog = OpenDialog::builder() + .transient_for_native(root) + .launch(OpenDialogSettings::default()) + .forward(&sender.input, |response| match response { + OpenDialogResponse::Accept(path) => WelcomeMsg::UpdateFlakePath(path), + OpenDialogResponse::Cancel => WelcomeMsg::Ignore, + }); + + + + let model = WelcomeModel { + hidden: true, + confpath: if Path::new("/etc/nixos/configuration.nix").exists() { Some(PathBuf::from("/etc/nixos/configuration.nix")) } else { None }, // parent_window.configpath.to_string(), + flake: false, + flakepath: None, + conf_dialog, + flake_dialog, + tracker: 0, + }; + + let widgets = view_output!(); + + widgets.btn.grab_focus(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + self.reset(); + match msg { + WelcomeMsg::Show => { + self.hidden = false; + } + WelcomeMsg::Close => { + if let Some(confpath) = &self.confpath { + let config = NscConfig { + systemconfig: confpath.to_string_lossy().to_string(), + flake: self.flakepath.as_ref().map(|x| x.to_string_lossy().to_string()), + }; + sender.output(AppMsg::LoadConfig(config)); + self.hidden = true; + } + } + WelcomeMsg::UpdateConfPath(s) => { + info!("Set configuration path to {}", s.to_string_lossy()); + self.set_confpath(Some(s)); + } + WelcomeMsg::UpdateFlakePath(s) => { + info!("Set flake path to {}", s.to_string_lossy()); + self.set_flakepath(Some(s)); + } + WelcomeMsg::ClearFlakePath => { + info!("Clear flake path"); + self.set_flakepath(None); + } + WelcomeMsg::OpenConf => { + self.conf_dialog.emit(OpenDialogMsg::Open) + } + WelcomeMsg::OpenFlake => { + self.flake_dialog.emit(OpenDialogMsg::Open) + } + WelcomeMsg::Ignore => {} + } + } +} diff --git a/src/ui/window.rs b/src/ui/window.rs index 64aff3c..0b41e6a 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -5,14 +5,14 @@ use adw::prelude::*; use edit_distance; use serde_json::Value; use spdx::Expression; -use crate::{parse::{packages::{Package, LicenseEnum, Platform}, cache::{uptodatelegacy, uptodateflake}, config::{NscConfig, getconfig, editconfig}}, ui::{installedpage::InstalledItem, pkgpage::PkgPageTypes}, APPINFO, config}; +use crate::{parse::{packages::{Package, LicenseEnum, Platform}, cache::{uptodatelegacy, uptodateflake}, config::{NscConfig, getconfig, editconfig}}, ui::{installedpage::InstalledItem, pkgpage::PkgPageInit, welcome::WelcomeMsg}, APPINFO, config}; use log::*; use super::{ categories::{PkgGroup, PkgCategory}, pkgtile::PkgTile, pkgpage::{PkgModel, PkgMsg, PkgInitModel, self, InstallType, WorkPkg}, - windowloading::{LoadErrorModel, LoadErrorMsg, WindowAsyncHandler, WindowAsyncHandlerMsg, CacheReturn}, searchpage::{SearchPageModel, SearchPageMsg, SearchItem}, installedpage::{InstalledPageModel, InstalledPageMsg}, updatepage::{UpdatePageModel, UpdatePageMsg, UpdateItem, UpdatePageInit}, about::{AboutPageModel, AboutPageMsg}, preferencespage::{PreferencesPageModel, PreferencesPageMsg}, categorypage::{CategoryPageModel, CategoryPageMsg}, categorytile::CategoryTile, + windowloading::{LoadErrorModel, LoadErrorMsg, WindowAsyncHandler, WindowAsyncHandlerMsg, CacheReturn}, searchpage::{SearchPageModel, SearchPageMsg, SearchItem}, installedpage::{InstalledPageModel, InstalledPageMsg}, updatepage::{UpdatePageModel, UpdatePageMsg, UpdateItem, UpdatePageInit}, about::{AboutPageModel, AboutPageMsg}, preferencespage::{PreferencesPageModel, PreferencesPageMsg}, categorypage::{CategoryPageModel, CategoryPageMsg}, categorytile::CategoryTile, welcome::WelcomeModel, }; #[derive(PartialEq)] @@ -91,6 +91,7 @@ pub enum AppMsg { UpdateFlake(Option), TryLoad, ReloadUpdate, + LoadConfig(NscConfig), Close, LoadError(String, String), Initialize(HashMap, Vec, HashMap, HashMap>, HashMap>, Option> /* profile pkgs */), @@ -375,6 +376,18 @@ impl Component for AppModel { debug!("userpkgtype: {:?}", userpkgtype); debug!("syspkgtype: {:?}", syspkgtype); + let (config, welcome) = if let Some(config) = getconfig() { + debug!("Got config: {:?}", config); + (config, false) + } else { + // Show welcome page + debug!("No config found"); + (NscConfig { + systemconfig: String::from("/etc/nixos/configuration.nix"), + flake: None, + }, true) + }; + let windowloading = WindowAsyncHandler::builder() .detach_worker(()) .forward(sender.input_sender(), identity); @@ -382,9 +395,10 @@ impl Component for AppModel { .launch(root.clone().upcast()) .forward(sender.input_sender(), identity); let pkgpage = PkgModel::builder() - .launch(PkgPageTypes { + .launch(PkgPageInit { userpkgs: userpkgtype.clone(), syspkgs: syspkgtype.clone(), + config: config.clone(), }) .forward(sender.input_sender(), identity); let searchpage = SearchPageModel::builder() @@ -397,12 +411,10 @@ impl Component for AppModel { .launch(userpkgtype.clone()) .forward(sender.input_sender(), identity); let updatepage = UpdatePageModel::builder() - .launch(UpdatePageInit { window: root.clone().upcast(), systype: syspkgtype.clone(), usertype: userpkgtype.clone() }) + .launch(UpdatePageInit { window: root.clone().upcast(), systype: syspkgtype.clone(), usertype: userpkgtype.clone(), config: config.clone() }) .forward(sender.input_sender(), identity); let viewstack = adw::ViewStack::new(); - let config = getconfig(); - let model = AppModel { application, mainwindow: root.clone(), @@ -438,7 +450,14 @@ impl Component for AppModel { tracker: 0, }; - model.windowloading.emit(WindowAsyncHandlerMsg::CheckCache(CacheReturn::Init, model.syspkgtype.clone(), model.userpkgtype.clone(), model.config.clone())); + if welcome { + let welcomepage = WelcomeModel::builder() + .launch(root.clone().upcast()) + .forward(sender.input_sender(), identity); + welcomepage.emit(WelcomeMsg::Show); + } else { + model.windowloading.emit(WindowAsyncHandlerMsg::CheckCache(CacheReturn::Init, model.syspkgtype.clone(), model.userpkgtype.clone(), model.config.clone())); + } let recbox = model.recommendedapps.widget(); let categorybox = model.categories.widget(); let viewstack = &model.viewstack; @@ -496,6 +515,15 @@ impl Component for AppModel { AppMsg::ReloadUpdate => { self.windowloading.emit(WindowAsyncHandlerMsg::CheckCache(CacheReturn::Update, self.syspkgtype.clone(), self.userpkgtype.clone(), self.config.clone())); } + AppMsg::LoadConfig(config) => { + self.config = config; + if let Err(e) = editconfig(self.config.clone()) { + warn!("Error editing config: {}", e); + } + self.pkgpage.emit(PkgMsg::UpdateConfig(self.config.clone())); + self.updatepage.emit(UpdatePageMsg::UpdateConfig(self.config.clone())); + self.windowloading.emit(WindowAsyncHandlerMsg::CheckCache(CacheReturn::Init, self.syspkgtype.clone(), self.userpkgtype.clone(), self.config.clone())); + } AppMsg::Close => { self.application.quit(); }