diff --git a/COSMIC-Backups-PoC.png b/COSMIC-Backups-PoC.png deleted file mode 100644 index 93b5ab4..0000000 Binary files a/COSMIC-Backups-PoC.png and /dev/null differ diff --git a/COSMIC-Backups.png b/COSMIC-Backups.png new file mode 100644 index 0000000..e2adcb9 Binary files /dev/null and b/COSMIC-Backups.png differ diff --git a/Cargo.toml b/Cargo.toml index 2868f5c..e68e242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,15 @@ version = "0.1.0" edition = "2021" license = "GPL-3.0" +[build-dependencies] +vergen = { version = "8", features = ["git", "gitcl"] } + [dependencies] i18n-embed-fl = "0.8" once_cell = "1.19.0" rust-embed = "8.3.0" +log = "0.4" +open = "5.0.2" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic.git" diff --git a/README.md b/README.md index 4d644f4..175752e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The current plan is to use Restic as the backend but for now the UI/UX is still being worked on. I have no idea if this will move forward past the init stage. -![COSMIC Backups PoC](COSMIC-Backups-PoC.png) +![COSMIC Backups](COSMIC-Backups.png) ## Install diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..be10241 --- /dev/null +++ b/build.rs @@ -0,0 +1,17 @@ +fn main() -> Result<(), Box> { + // Rebuild if i18n files change + println!("cargo:rerun-if-changed=i18n"); + + // Emit version information (if not cached by just vendor) + let mut vergen = vergen::EmitBuilder::builder(); + println!("cargo:rerun-if-env-changed=VERGEN_GIT_COMMIT_DATE"); + if std::env::var_os("VERGEN_GIT_COMMIT_DATE").is_none() { + vergen.git_commit_date(); + } + println!("cargo:rerun-if-env-changed=VERGEN_GIT_SHA"); + if std::env::var_os("VERGEN_GIT_SHA").is_none() { + vergen.git_sha(false); + } + vergen.fail_on_error().emit()?; + Ok(()) +} diff --git a/i18n/en/cosmic_backups.ftl b/i18n/en/cosmic_backups.ftl index 33cb36b..da6c2ec 100644 --- a/i18n/en/cosmic_backups.ftl +++ b/i18n/en/cosmic_backups.ftl @@ -1,2 +1,23 @@ +cosmic-backups = COSMIC Backups app-title = COSMIC Backups welcome = Backup and backup often! ✨ + +# Context Pages + +## About +git-description = Git commit {$hash} on {$date} + +# Menu + +## File +file = File +new-window = New window +quit = Quit + +## Edit +edit = Edit +cut = Cut + +## View +view = View +menu-about = About COSMIC Backups... diff --git a/src/app.rs b/src/app.rs index 5e10c01..a84ff60 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,35 +1,125 @@ // SPDX-License-Identifier: GPL-3.0-only +use std::{ + collections::{HashMap}, + env, + process, +}; use crate::fl; use cosmic::app::{Command, Core}; +use cosmic::{ + cosmic_theme, ApplicationExt, + iced::{Alignment, Length}, +}; use cosmic::iced::alignment::{Horizontal, Vertical}; -use cosmic::iced::Length; +use cosmic::iced::window; +use cosmic::iced_core::keyboard::Key; +use cosmic::widget::menu::{ + action::{MenuAction}, + key_bind::{KeyBind, Modifier}, +}; +use cosmic::widget::segmented_button::Entity; use cosmic::{widget, Application, Element}; +pub mod menu; + /// This is the struct that represents your application. /// It is used to define the data that will be used by your application. -#[derive(Clone, Default)] -pub struct CosmicBackups { +#[derive(Clone)] +pub struct App { /// This is the core of your application, it is used to communicate with the Cosmic runtime. /// It is used to send messages to your application, and to access the resources of the Cosmic runtime. core: Core, + context_page: ContextPage, + key_binds: HashMap, } /// This is the enum that contains all the possible variants that your application will need to transmit messages. /// This is used to communicate between the different parts of your application. /// If your application does not need to send messages, you can use an empty enum or `()`. #[derive(Debug, Clone)] -pub enum Message {} - -/// Implement the `Application` trait for your application. -/// This is where you define the behavior of your application. -/// -/// The `Application` trait requires you to define the following types and constants: -/// - `Executor` is the executor that will be used to run your application. -/// - `Flags` is the data that your application needs to use before it starts. -/// - `Message` is the enum that contains all the possible variants that your application will need to transmit messages. -/// - `APP_ID` is the unique identifier of your application. -impl Application for CosmicBackups { +pub enum Message { + // Cut(Option), + ToggleContextPage(ContextPage), + LaunchUrl(String), + WindowClose, + WindowNew, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ContextPage { + About, +} + +impl ContextPage { + fn title(&self) -> String { + match self { + Self::About => String::new(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Action { + About, + // Cut, + WindowClose, + WindowNew, +} + +impl MenuAction for Action { + type Message = Message; + fn message(&self, _entity_opt: Option) -> Self::Message { + match self { + Action::About => Message::ToggleContextPage(ContextPage::About), + // Action::Cut => Message::Cut(entity_opt), + Action::WindowClose => Message::WindowClose, + Action::WindowNew => Message::WindowNew, + } + } +} + +impl App { +// fn update_config(&mut self) -> Command> { +// cosmic::app::command::set_theme(self.config.app_theme.theme()) +// } + + fn about(&self) -> Element { + let cosmic_theme::Spacing { space_xxs, .. } = cosmic::theme::active().cosmic().spacing; + let repository = "https://github.com/ahoneybun/cosmic-backups"; + let hash = env!("VERGEN_GIT_SHA"); + let short_hash: String = hash.chars().take(7).collect(); + let date = env!("VERGEN_GIT_COMMIT_DATE"); + widget::column::with_children(vec![ + widget::svg(widget::svg::Handle::from_memory( + &include_bytes!( + "../res/icons/hicolor/128x128/apps/com.example.CosmicAppTemplate.svg" + )[..], + )) + .into(), + widget::text::title3(fl!("cosmic-backups")).into(), + widget::button::link(repository) + .on_press(Message::LaunchUrl(repository.to_string())) + .padding(0) + .into(), + widget::button::link(fl!( + "git-description", + hash = short_hash.as_str(), + date = date + )) + .on_press(Message::LaunchUrl(format!("{}/commits/{}", repository, hash))) + .padding(0) + .into(), + ]) + .align_items(Alignment::Center) + .spacing(space_xxs) + .into() + } + +} + + +impl Application for App { type Executor = cosmic::executor::Default; type Flags = (); @@ -47,29 +137,30 @@ impl Application for CosmicBackups { } /// This is the header of your application, it can be used to display the title of your application. - fn header_center(&self) -> Vec> { - vec![widget::text::heading(fl!("app-title")).into()] + fn header_start(&self) -> Vec> { + vec![menu::menu_bar(&self.key_binds)] + } + + fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + let app = App { + core, + context_page: ContextPage::About, + key_binds: key_binds(), + }; + + (app, Command::none()) } - /// This is the entry point of your application, it is where you initialize your application. - /// - /// Any work that needs to be done before the application starts should be done here. - /// - /// - `core` is used to passed on for you by libcosmic to use in the core of your own application. - /// - `flags` is used to pass in any data that your application needs to use before it starts. - /// - `Command` type is used to send messages to your application. `Command::none()` can be used to send no messages to your application. - fn init(core: Core, _flags: Self::Flags) -> (Self, Command) { - let example = CosmicBackups { core }; - - (example, Command::none()) + fn context_drawer(&self) -> Option> { + if !self.core.window.show_context { + return None; + } + + Some(match self.context_page { + ContextPage::About => self.about(), + }) } - /// This is the main view of your application, it is the root of your widget tree. - /// - /// The `Element` type is used to represent the visual elements of your application, - /// it has a `Message` associated with it, which dictates what type of message it can send. - /// - /// To get a better sense of which widgets are available, check out the `widget` module. fn view(&self) -> Element { widget::container(widget::text::title1(fl!("welcome"))) .width(Length::Fill) @@ -78,4 +169,65 @@ impl Application for CosmicBackups { .align_y(Vertical::Center) .into() } + + /// Handle application events here. + fn update(&mut self, message: Self::Message) -> Command { + + match message { + Message::ToggleContextPage(context_page) => { + //TODO: ensure context menus are closed + if self.context_page == context_page { + self.core.window.show_context = !self.core.window.show_context; + } else { + self.context_page = context_page; + self.core.window.show_context = true; + } + self.set_context_title(context_page.title()); + } + Message::WindowClose => { + return window::close(window::Id::MAIN); + } + Message::WindowNew => match env::current_exe() { + Ok(exe) => match process::Command::new(&exe).spawn() { + Ok(_child) => {} + Err(err) => { + eprintln!("failed to execute {:?}: {}", exe, err); + } + }, + Err(err) => { + eprintln!("failed to get current executable path: {}", err); + } + }, + Message::LaunchUrl(url) => match open::that_detached(&url) { + Ok(()) => {} + Err(err) => { + log::warn!("failed to open {:?}: {}", url, err); + } + }, + } + + Command::none() + + } +} + +pub fn key_binds() -> HashMap { + let mut key_binds = HashMap::new(); + + macro_rules! bind { + ([$($modifier:ident),* $(,)?], $key:expr, $action:ident) => {{ + key_binds.insert( + KeyBind { + modifiers: vec![$(Modifier::$modifier),*], + key: $key, + }, + Action::$action, + ); + }}; + } + + bind!([Ctrl], Key::Character("w".into()), WindowClose); + bind!([Ctrl, Shift], Key::Character("n".into()), WindowNew); + + key_binds } diff --git a/src/app/menu.rs b/src/app/menu.rs new file mode 100644 index 0000000..8d1532f --- /dev/null +++ b/src/app/menu.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::collections::HashMap; + +use cosmic::widget::menu::key_bind::KeyBind; +use cosmic::{ + widget::menu::{items, root, Item, ItemHeight, ItemWidth, MenuBar, Tree}, + Element, +}; + +use crate::{ + app::{Action, Message}, + fl, +}; + +pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message> { + MenuBar::new(vec![Tree::with_children( + root(fl!("file")), + items( + key_binds, + vec![ + Item::Button(fl!("new-window"), Action::WindowNew), + Item::Button(fl!("quit"), Action::WindowClose), + ], + ), + ), + // Tree::with_children( + // root(fl!("edit")), + // items( + // key_binds, + // vec![ + // Item::Button(fl!("cut"), Action::Cut), + // ], + // ), + // ), + Tree::with_children( + root(fl!("view")), + items( + key_binds, + vec![ + Item::Button(fl!("menu-about"), Action::About), + ], + ), + ), + ]) + .item_height(ItemHeight::Dynamic(40)) + .item_width(ItemWidth::Uniform(240)) + .spacing(4.0) + .into() +} diff --git a/src/main.rs b/src/main.rs index d142887..b0f01f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only -use app::CosmicBackups; +use crate::app::App; /// The `app` module is used by convention to indicate the main component of our application. mod app; mod core; @@ -12,5 +12,5 @@ mod core; /// If your app does not need any flags, you can pass in `()`. fn main() -> cosmic::iced::Result { let settings = cosmic::app::Settings::default(); - cosmic::app::run::(settings, ()) + cosmic::app::run::(settings, ()) }