commit 188d6d1a3ec81ca98f82b807ba3bb848b9c5fb1e Author: itwrx Date: Sun Jun 25 16:34:23 2023 -0500 initial commit diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d9287ce --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## 0.9.1 - 6-25-23 + +### Changed +- separate logging procs into Error and Info varieties. (ITwrx) + +### Added +- ask-pass path resolution for various distros. (ITwrx) +- log sqlite3 access error/dependancy requirement on failed create/read. (ITwrx) \ No newline at end of file diff --git a/ez_bkup.nim b/ez_bkup.nim new file mode 100644 index 0000000..e718d89 --- /dev/null +++ b/ez_bkup.nim @@ -0,0 +1,69 @@ +#[Copyright 2023 ITwrx. +This file is part of EZ-Bkup. +EZ-Bkup is released under the General Public License 3.0. +See COPYING or for details.]# + +import owlkettle +import "models/routine", "shared" +import views/[edit_routine_dialog, routine_list, app_menu_button] + +const APP_NAME = "EZ-Bkup" +let databasePath = appPath & "/ez-bkup.sqlite" + +viewable App: + ## The main application + routineModel: RoutineModel ## The RoutineModel that stores all routines. + +method view(app: AppState): Widget {.locks: "unknown".} = + result = gui: + Window: + title = APP_NAME + defaultSize = (1024, 768) + HeaderBar {.addTitlebar.}: + Icon {.addLeft.}: + name = "EZ-Bkup-icon" + pixelSize = 40 + margin = 4 + # Button to open the main menu + AppMenuButton {.addRight.}: + routineModel = app.routineModel + Box: + orient = OrientY + spacing = 6 + #don't use available vertical space from parent. Only what's needed for children. + Box(orient = OrientX) {.expand: false.}: + Label: + text = "Bkup Routines" + xAlign = 0.05 + useMarkup = true + #expand horizontally to push buttons to the right. + Box(orient = OrientX) {.expand: true.} + #expanding box containing buttons would fight the previous box for available horiz space placing buttons in the middle. + Box(orient = OrientX, spacing = 6, margin = 6) {.expand: false.}: + # Button to create a new routine + Button: + style = [ButtonSuggested] + text = "+ Routine" + proc clicked() = + ## Opens the EditRoutineDialog for creating a new routine + let (res, state) = app.open: gui: + EditRoutineDialog(mode = EditRoutineCreate) + if res.kind == DialogAccept: + # The "Create" button was clicked + app.routineModel.add(EditRoutineDialogState(state).routine) + + # Main content of the window: Contains the list of all routines. + Box: + orient = OrientY + margin = 6 + spacing = 12 + Frame: + RoutineList: + routineModel = app.routineModel + +when isMainModule: + # Entry point for the application. + # Loads the model from the database and starts the application. + let model = newRoutineModel(databasePath) + brew("org.itwrx.EZ-Bkup", gui(App(routineModel = model)), icons=["icons/"], stylesheets=[loadStylesheet("styles.css")]) + \ No newline at end of file diff --git a/icons.tar.gz b/icons.tar.gz new file mode 100644 index 0000000..5c4e006 Binary files /dev/null and b/icons.tar.gz differ diff --git a/icons/EZ-Bkup-icon.png b/icons/EZ-Bkup-icon.png new file mode 100644 index 0000000..8834ba8 Binary files /dev/null and b/icons/EZ-Bkup-icon.png differ diff --git a/icons/entity-edit-dark-theme.svg b/icons/entity-edit-dark-theme.svg new file mode 100644 index 0000000..6697db5 --- /dev/null +++ b/icons/entity-edit-dark-theme.svg @@ -0,0 +1,45 @@ + + + + + + + + + diff --git a/icons/entity-edit.svg b/icons/entity-edit.svg new file mode 100644 index 0000000..2574d1d --- /dev/null +++ b/icons/entity-edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/models/routine.nim b/models/routine.nim new file mode 100644 index 0000000..737d9e3 --- /dev/null +++ b/models/routine.nim @@ -0,0 +1,187 @@ +import std/[tables, strutils, sugar, hashes, algorithm, os] +from std/sequtils import toSeq +import tiny_sqlite +import "../shared" + +# We create a custom type for representing the ID of a routine. +# This way Nim's type system automatically prevents us from using it as a regular integer and +# from confusing it with the ID of another entity. + +type RoutineId* = distinct int64 + +proc `==`*(a, b: RoutineId): bool {.borrow.} +proc hash*(id: RoutineId): Hash {.borrow.} +proc `$`*(id: RoutineId): string = + result = "routine" & $int64(id) + +type Routine* = object + id*: RoutineId + name*: string + selected*: bool + #selByDef == selected by default. + selByDef*: bool + sources*: seq[string] + destinations*: seq[string] + +proc stringToSeq(commaSepString: string):seq[string] = + #return toSeq(commaSepString.split(',')) + var newSeq: seq[string] + if hasCommas(commaSepString) == true: + newSeq = toSeq(commaSepString.split(',')) + else: + newSeq = @[commaSepString] + + return newSeq + +proc seqToString(strSeq: seq[string]):string = + #return strSeq.join(",") + var newStr: string + if strSeq.len >= 1: + newStr = strSeq.join(",") + else: + newStr = "" + + return newStr + +#[proc matches*(routine: Routine, filter: string): bool = + ## Checks if the routine matches the given filter. + ## This function is used to search the list of routines. + filter.toLowerAscii() in toLowerAscii(routine.name)]# + +type RoutineModel* = ref object + ## Model for storing all routines. We model this as a ref object, so that changes + ## made by any widget are also known to all other widgets that use the model. + + db: DbConn + routines*: Table[RoutineId, Routine] + +proc cmpRoutines(a, b: Routine): int = + cmp(a.name, b.name) + + +#this is our stand-in for the search proc. returns a seq so we can iterate in routine_list. +proc routineSeq*(model: RoutineModel): seq[Routine] = + var routineSeq: seq[Routine] + ## Returns a seq of all routines. "id" is necessary to loop through table. + for id, routine in model.routines: + routineSeq.add(routine) + + routineSeq.sort(cmpRoutines, Ascending) + + #echo routineSeq + + return routineSeq + +let homePath = expandTilde("~") + +proc newRoutineModel*(path: string = homePath & "/.ez-bkup/ez-bkup.sqlite"): RoutineModel = + ## Load a RoutineModel from a database + + var db: DbConn + try: + db = openDatabase(path) + except: + writeErrorToLog("Unable to open and/or create sqlite3 database. Please make sure sqlite3 is installed.") + db = openDatabase("") + + # Create the Routine table + db.exec(""" + CREATE TABLE IF NOT EXISTS Routine( + id INTEGER PRIMARY KEY, + name TEXT, + selByDef INTEGER, + sources TEXT, + destinations TEXT + ) + """) + + # Load all existing routines into RoutineModel.routines + var routines = initTable[RoutineId, Routine]() + for row in db.iterate("SELECT id, name, selByDef, sources, destinations FROM Routine"): + let (id, name, selByDef, sources, destinations) = row.unpack((RoutineId, string, bool, string, string)) + + var sourcesSeq: seq[string] + #no sources exist in DB. + if sources == "": + sourcesSeq = @[] + else: + #convert "sources" comma-separated string to seq[string]. + sourcesSeq = stringToSeq(sources) + + var destinationsSeq: seq[string] + #no destinations exist in DB. + if destinations == "": + destinationsSeq = @[] + else: + #convert "destinations" comma-separated string to seq[string]. + destinationsSeq = stringToSeq(destinations) + + routines[id] = Routine(id: id, name: name, selByDef: selByDef, sources: sourcesSeq, destinations: destinationsSeq) + + #echo "newRoutineModel:" + #echo routines + + result = RoutineModel(db: db, routines: routines) + +proc add*(model: RoutineModel, routine: Routine) = + ## Adds a new routine to the model + + #convert routine.sources seq[string] to a comma-separated string to store as TEXT in sqlite. + let stringSources = seqToString(routine.sources) + + #convert routine.destinations seq[string] to a comma-separated string to store as TEXT in sqlite. + let stringDestinations = seqToString(routine.destinations) + + # Insert new routine into database + model.db.exec( + "INSERT INTO Routine (name, selByDef, sources, destinations) VALUES (?, ?, ?, ?)", + routine.name, routine.selByDef, stringSources, stringDestinations + ) + + # Insert new routine into RoutineModel.routines + let id = RoutineId(model.db.lastInsertRowId) + model.routines[id] = routine.dup(id = id) + +proc update*(model: RoutineModel, routine: Routine) = + ## Updates an existing routine. Routines are compared by their ID. + + #convert routine.sources seq[string] to a comma-separated string to store as TEXT in sqlite. + let stringSources = seqToString(routine.sources) + + #convert routine.destinations seq[string] to a comma-separated string to store as TEXT in sqlite. + let stringDestinations = seqToString(routine.destinations) + + # Update routine in database + model.db.exec( + "UPDATE Routine SET name = ?, selByDef = ?, sources = ?, destinations = ? WHERE id = ?", + routine.name, routine.selByDef, stringSources, stringDestinations, routine.id + ) + + # Update RoutineModel.routines + model.routines[routine.id] = routine + +#[proc search*(model: UserModel, filter: string): seq[User] {.locks: 0.} = + ## Returns a seq of all users that match the given filter. + for id, user in model.users: + if user.matches(filter): + result.add(user) + + result.sort((a, b: User) => cmp(a.lastName, b.lastName))]# + +proc delete*(model: RoutineModel, id: RoutineId) = + ## Deletes the routine with the given ID + + # Delete routine from database + model.db.exec("DELETE FROM Routine WHERE id = ?", id) + + # Update RoutineModel.routines + model.routines.del(id) + +proc clear*(model: RoutineModel) = + ## Deletes all routines + + # Delete all routines from database + model.db.exec("DELETE FROM Routine") + + # Update RoutineModel.routines + model.routines.clear() \ No newline at end of file diff --git a/shared.nim b/shared.nim new file mode 100644 index 0000000..7276958 --- /dev/null +++ b/shared.nim @@ -0,0 +1,83 @@ +#[Copyright 2023 ITwrx. +This file is part of EZ-Bkup. +EZ-Bkup is released under the General Public License 3.0. +See COPYING or for details.]# + +import logging, times, os, distros +#import "models/routine" + +#store the RoutineIds of ea. Routine selected for the current Bkup run. +#[type SelectedRoutine* = object + id*: RoutineId + name*: string + sources*: seq[string] + destinations*: seq[string] + +type CurrentRun* = object + status*: string]# + +let appPath* = getHomeDir() & ".ez-bkup" + +if not dirExists(appPath): + createDir(appPath) + +var logger = newFileLogger(appPath & "/errors.log") +let dt = now() +let nowDT = dt.format("M-d-YYYY h:mm:ss tt") +var logMsg: string + +proc writeErrorToLog*(logMsg: string) = + logger.log(lvlError, logMsg) + +proc writeInfoToLog*(logMsg: string) = + logger.log(lvlInfo, logMsg) + +proc hasCommas*(filename: string):bool = + ',' in filename + +proc getAskPassPath*(): string = + var askPassPath: string + if detectOs(Fedora): + if fileExists("/usr/libexec/openssh/ssh-askpass"): + askPassPath = "/usr/libexec/openssh/ssh-askpass" + else: + writeErrorToLog("No ssh-askpass binary found. Please run 'dnf install openssh-askpass'") + askPassPath = "" + elif detectOs(Ubuntu): + if fileExists("/usr/lib/openssh/gnome-ssh-askpass"): + askPassPath = "/usr/lib/openssh/gnome-ssh-askpass" + else: + writeErrorToLog("No ssh-askpass binary found. Please run 'sudo apt install ssh-askpass-gnome'.") + askPassPath = "" + #save this for a musl build. + #[elif detectOs(Alpine): + if fileExists("/usr/lib/ssh/gtk-ssh-askpass"): + askPassPath = "/usr/lib/ssh/gtk-ssh-askpass" + else: + writeErrorToLog("No ssh-askpass binary found. Please run 'apk add gtk-ssh-askpass'.") + askPassPath = ""]# + elif detectOs(ArchLinux): + if fileExists("/usr/lib/ssh/ssh-askpass"): + askPassPath = "/usr/lib/ssh/ssh-askpass" + else: + writeErrorToLog("No ssh-askpass binary found. Please run 'pacman -S x11-ssh-askpass', or similar.") + askPassPath = "" + elif detectOs(Linux): + if fileExists("/usr/libexec/openssh/ssh-askpass"): + askPassPath = "/usr/libexec/openssh/ssh-askpass" + elif fileExists("/usr/lib/openssh/gnome-ssh-askpass"): + askPassPath = "/usr/lib/openssh/gnome-ssh-askpass" + elif fileExists("/usr/lib/ssh/gtk-ssh-askpass"): + askPassPath = "/usr/lib/ssh/gtk-ssh-askpass" + elif fileExists("/usr/lib/ssh/ssh-askpass"): + askPassPath = "/usr/lib/ssh/ssh-askpass" + else: + writeErrorToLog("No ssh-askpass binary found. Please install an ssh-askpass package for your distro, and let us know if EZ-Bkup still can't detect it's location.") + askPassPath = "" + else: + writeErrorToLog("Your OS does not appear to be supported at this time. If you are getting this error and you are using a x86_64 gnu libc-based linux distribution please report this issue. Please include the path to your ssh-askpass binary, as well.") + askPassPath = "" + + return askPassPath + + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..c0b9d29 --- /dev/null +++ b/styles.css @@ -0,0 +1,5 @@ +.aboutdialog box stack {font-size: 22px;} +.aboutdialog box label {font-size: 20px;} +.aboutdialog headerbar label {font-size: 16px;} +.aboutdialog stack scrolledwindow viewport grid {font-size: 22px;} +.error {color: #ff6b6b;} diff --git a/views/app_menu_button.nim b/views/app_menu_button.nim new file mode 100644 index 0000000..7422a28 --- /dev/null +++ b/views/app_menu_button.nim @@ -0,0 +1,42 @@ +#[Copyright 2023 ITwrx. +This file is part of EZ-Bkup. +EZ-Bkup is released under the General Public License 3.0. +See COPYING or for details.]# + +import owlkettle +import ../models/routine + +viewable AppMenuButton: + ## A button that opens the main menu of the application + + routineModel: RoutineModel + +method view(button: AppMenuButtonState): Widget {.locks: "unknown".} = + result = gui: + MenuButton: + icon = "open-menu-symbolic" + + # A menu is created using the PopoverMenu widget. + # It allows us to create menus & submenus. + PopoverMenu: + Box: + orient = OrientY + Box(orient = OrientX, spacing=6, margin=6) {.expand: true.}: + Button: + icon = "help-about-symbolic" + style = [ButtonSuggested] + proc clicked() = + discard button.app.open: gui: + AboutDialog: + programName = "EZ-Bkup" + logo = "EZ-Bkup-icon" + style = [StyleClass("about-dialog")] + version = "0.9.0" + credits = @{ + "Created By": @["https://ITwrx.org"], + "License": @["GPLv3"] + } + Label: + text = "About EZ-Bkup" + +export AppMenuButton diff --git a/views/edit_routine_dialog.nim b/views/edit_routine_dialog.nim new file mode 100644 index 0000000..c46322b --- /dev/null +++ b/views/edit_routine_dialog.nim @@ -0,0 +1,41 @@ +#[Copyright 2023 ITwrx. +This file is part of EZ-Bkup. +EZ-Bkup is released under the General Public License 3.0. +See COPYING or for details.]# + +import owlkettle +import ../models/routine +import routine_editor + +type EditRoutineDialogMode* = enum + EditRoutineCreate = "Create" + EditRoutineUpdate = "Update" + +viewable EditRoutineDialog: + ## A dialog for editing a routine. We use the same dialog for creating and updating + ## a routine. Since we want to use different titles and labels for the buttons in each case, + ## the EditRoutineDialog.mode field specifies the purpose of the dialog. + + routine: Routine ## The routine being edited + mode: EditRoutineDialogMode ## Purpose of the dialog (create/update) + +method view(dialog: EditRoutineDialogState): Widget {.locks: "unknown".} = + result = gui: + Dialog: + title = $dialog.mode & " Routine" + defaultSize = (800, 600) + DialogButton {.addButton.}: + # Create / Update Button + text = $dialog.mode + style = [ButtonSuggested] + res = DialogAccept + DialogButton {.addButton.}: + text = "Cancel" + res = DialogCancel + # Content + RoutineEditor: + routine = dialog.routine + proc changed(routine: Routine) = + dialog.routine = routine + +export EditRoutineDialog, EditRoutineDialogState diff --git a/views/routine_editor.nim b/views/routine_editor.nim new file mode 100644 index 0000000..8f5afe8 --- /dev/null +++ b/views/routine_editor.nim @@ -0,0 +1,221 @@ +#[Copyright 2023 ITwrx. +This file is part of EZ-Bkup. +EZ-Bkup is released under the General Public License 3.0. +See COPYING or for details.]# + +import owlkettle +import "../models/routine", "../shared" + +viewable RoutineEditor: + ## A form for editing a Routine + + routine: Routine ## The routine that is being edited + + # Since Routine is passed by value, we need to notify the parent widget of + # any changes to the routine. The changed callback + proc changed(routine: Routine) + +method view(editor: RoutineEditorState): Widget {.locks: "unknown".} = + result = gui: + #must have top level container that everything else is inside of. + Box: + orient = OrientY + margin = 12 + spacing = 12 + Box {.expand: false.}: + orient = OrientX + spacing = 6 + Label {.expand: false.}: + text = "Name:" + xAlign = 0 + useMarkup = true + Entry {.expand: false.}: + text = editor.routine.name + xAlign = 0 + proc changed(text: string) = + editor.routine.name = text + # Call the changed callback + if not editor.changed.isNil: + editor.changed.callback(editor.routine) + Box {.expand: false.}: + orient = OrientX + spacing = 6 + Label {.expand: false.}: + #later, this may be "selected by default?", instead. + #"selected" checkboxes wiould be added to the list view. + #to allow the user to select which routines are used for a given run. + text = "Enabled?" + xAlign = 0 + useMarkup = true + Switch {.expand: false.}: + state = editor.routine.selByDef + proc changed(state: bool) = + editor.routine.selByDef = state + if not editor.changed.isNil: + editor.changed.callback(editor.routine) + Label {.expand: false.}: + text = "Sources:" + xAlign = 0 + useMarkup = true + Box {.expand: false.}: + spacing = 6 + Button {.expand: false.}: + text = "+ File" + style = [ButtonSuggested] + proc clicked() = + let (res, state) = editor.app.open: gui: + FileChooserDialog: + title = "Add Source Files to Routine" + action = FileChooserOpen + selectMultiple = true + DialogButton {.addButton.}: + text = "Cancel" + res = DialogCancel + DialogButton {.addButton.}: + text = "Confirm" + res = DialogAccept + style = [ButtonSuggested] + if res.kind == DialogAccept: + for filename in FileChooserDialogState(state).filenames: + if hasCommas(filename): + discard editor.app.open: gui: + MessageDialog: + message = "Commas in Source filename/path are not supported." + style = [StyleClass("error")] + DialogButton {.addButton.}: + text = "Ok" + res = DialogAccept + elif filename in editor.routine.sources: + discard editor.app.open: gui: + MessageDialog: + message = "Duplicate Routine Source filenames (" & filename & ") are not supported." + style = [StyleClass("error")] + DialogButton {.addButton.}: + text = "Ok" + res = DialogAccept + else: + editor.routine.sources.add(filename) + if not editor.changed.isNil: + editor.changed.callback(editor.routine) + Button {.expand: false.}: + text = "+ Folder" + style = [ButtonSuggested] + proc clicked() = + let (res, state) = editor.app.open: gui: + FileChooserDialog: + title = "Add Source Folders to Routine" + action = FileChooserSelectFolder + selectMultiple = true + DialogButton {.addButton.}: + text = "Cancel" + res = DialogCancel + DialogButton {.addButton.}: + text = "Confirm" + res = DialogAccept + style = [ButtonSuggested] + if res.kind == DialogAccept: + for filename in FileChooserDialogState(state).filenames: + if hasCommas(filename): + discard editor.app.open: gui: + MessageDialog: + message = "Commas in Source filename/path are not supported." + style = [StyleClass("error")] + DialogButton {.addButton.}: + text = "Ok" + res = DialogAccept + elif filename in editor.routine.sources: + discard editor.app.open: gui: + MessageDialog: + message = "Duplicate Routine Source (" & filename & ") not supported." + style = [StyleClass("error")] + DialogButton {.addButton.}: + text = "Ok" + res = DialogAccept + else: + editor.routine.sources.add(filename) + if not editor.changed.isNil: + editor.changed.callback(editor.routine) + Box {.expand: false.}: + orient = OrientY + margin = 6 + spacing = 6 + #routineSeq returns an empty seq when no sources exist. + if editor.routine.sources.len != 0: + for it, routineSource in editor.routine.sources: + Box: + orient = OrientX + spacing = 6 + Label: + text = routineSource + xAlign = 0 + Button {.expand: false.}: + icon = "user-trash-symbolic" + proc clicked() = + editor.routine.sources.delete(it) + if not editor.changed.isNil: + editor.changed.callback(editor.routine) + Label {.expand: false.}: + text = "Destinations:" + xAlign = 0 + useMarkup = true + Box {.expand: false.}: + spacing = 6 + Button {.expand: false.}: + text = "+ Folder" + style = [ButtonSuggested] + proc clicked() = + let (res, state) = editor.app.open: gui: + FileChooserDialog: + title = "Add Destination Folders to Routine" + action = FileChooserSelectFolder + selectMultiple = true + DialogButton {.addButton.}: + text = "Cancel" + res = DialogCancel + DialogButton {.addButton.}: + text = "Confirm" + res = DialogAccept + style = [ButtonSuggested] + if res.kind == DialogAccept: + for filename in FileChooserDialogState(state).filenames: + if hasCommas(filename): + discard editor.app.open: gui: + MessageDialog: + message = "Commas in Destination filename/path are not supported." + style = [StyleClass("error")] + DialogButton {.addButton.}: + text = "Ok" + res = DialogAccept + elif filename in editor.routine.destinations: + discard editor.app.open: gui: + MessageDialog: + message = "Duplicate Routine Destination (" & filename & ") not supported." + style = [StyleClass("error")] + DialogButton {.addButton.}: + text = "Ok" + res = DialogAccept + else: + editor.routine.destinations.add(filename) + if not editor.changed.isNil: + editor.changed.callback(editor.routine) + Box {.expand: false.}: + orient = OrientY + margin = 6 + spacing = 6 + #routineSeq returns an empty seq when no destinations exist. + if editor.routine.destinations.len != 0: + for it, routineDestination in editor.routine.destinations: + Box: + orient = OrientX + spacing = 6 + Label: + text = routineDestination + xAlign = 0 + Button {.expand: false.}: + icon = "user-trash-symbolic" + proc clicked() = + editor.routine.destinations.delete(it) + if not editor.changed.isNil: + editor.changed.callback(editor.routine) + +export RoutineEditor \ No newline at end of file diff --git a/views/routine_list.nim b/views/routine_list.nim new file mode 100644 index 0000000..01e648b --- /dev/null +++ b/views/routine_list.nim @@ -0,0 +1,219 @@ +#[Copyright 2023 ITwrx. +This file is part of EZ-Bkup. +EZ-Bkup is released under the General Public License 3.0. +See COPYING or for details.]# + +import owlkettle +import std/osproc +import edit_routine_dialog +import "../models/routine", "../shared" + +##preload selectedRoutines using routines' selByDef field value from DB. +#var selectedRoutines: seq[SelectedRoutine] + +viewable RoutineList: + ## Displays a list of routines + + #filter: string ## Filter used to search for routines + routineModel: RoutineModel ## Model of all routines + #selectedRoutines: selectedRoutines + #selectedRoutines: seq[SelectedRoutine] + runStatus: string + + #proc changed(currentRun: CurrentRun) + proc changed(state: bool) + +method view(list: RoutineListState): Widget {.locks: "unknown".} = + result = gui: + ScrolledWindow: + Box: + orient = OrientY + if list.routineModel.routineSeq().len() > 0: + ListBox: + #for routine in list.model.search(list.filter): + for it, routine in list.routineModel.routineSeq(): + Box: + orient = OrientY + margin = 6 + spacing = 6 + Box: + orient = OrientX + margin = 6 + spacing = 6 + #[Switch {.expand: false.}: + state = routine.selByDef + proc changed(state: bool) = + #if state == true: + #list.selectedRoutines.ids.add(routine.id) + + if not list.changed.isNil: + list.changed.callback(list.selectedRoutines) ]# + #[CheckButton {.expand: false.}: + state = routine.selected + #if routine.selected == true: + #echo list.currentRun.selected + #list.currentRun.selected[it] = true + #state = t + #state = list.currentRun.selectedRoutines[it] + proc changed(state: bool) = + list.currentRun.selectedRoutines.add(routine.id) + if routine.id in list.currentRun.selectedRoutines: + = state + #echo "hehe" + #list.currentRun.selectedRoutines[it] = state ]# + + Label: + #text = "" & routine.name & "" + text = "" & routine.name & "" + xAlign = 0 # Align left + useMarkup = true + # Edit Button + Button {.expand: false.}: + #icon = "entity-edit" + icon = "entity-edit-dark-theme" + proc clicked() = + ## Opens the EditRoutineDialog for updating the existing routine + let (res, state) = list.app.open: gui: + EditRoutineDialog: + routine = routine + mode = EditRoutineUpdate + if res.kind == DialogAccept: + # The "Update" button was clicked + list.routineModel.update(EditRoutineDialogState(state).routine) + # Delete Button + Button {.expand: false.}: + icon = "user-trash-symbolic" + proc clicked() = + list.routineModel.delete(routine.id) + if routine.selByDef: + Box: + orient = OrientY + margin = 6 + spacing = 6 + Label: + text = "Sources:" + xAlign = 0 + useMarkup = true + #routineSeq returns an empty string when no sources exist. + if routine.sources.len != 0: + for it, routineSource in routine.sources: + Box: + orient = OrientX + spacing = 6 + margin = 6 + Label: + text = routineSource + xAlign = 0 + else: + Box: + orient = OrientX + spacing = 6 + margin = 6 + Label: + text = "Routine will be ignored. Source required." + xAlign = 0 + useMarkup = true + Label: + text = "Destinations:" + xAlign = 0 + useMarkup = true + #routineSeq returns an empty string when no destinations exist. + if routine.destinations.len != 0: + for it, routineDestination in routine.destinations: + Box: + orient = OrientX + spacing = 6 + margin = 6 + Label: + text = routineDestination + xAlign = 0 + else: + Box: + orient = OrientX + spacing = 6 + margin = 6 + Label: + text = "Routine will be ignored. Destination required." + xAlign = 0 + useMarkup = true + Box {.expand: false.}: + orient = OrientX + spacing = 6 + margin = 6 + Label {.expand: false.}: + text = "Status: " + xAlign = 0 + useMarkup = true + Label: + if list.runStatus != "": + text = list.runStatus + else: + text = "Waiting patiently..." + xAlign = 0 + margin = 6 + useMarkup = true + # Run Button + Button {.expand: false.}: + Box: + orient = OrientX + spacing = 6 + margin = 6 + Icon: + name = "media-floppy" + pixelSize = 40 + margin = 4 + Label {.expand: false.}: + text = "Run Bkup!" + xAlign = 0 + useMarkup = true + proc clicked() = + list.runStatus = "Running Bkup..." + var rsyncRun: tuple[output: string, exitCode: int] + var rsyncErrors: seq[string] + var routineRunCount = 0 + for routine in list.routineModel.routineSeq(): + #using this as "selected" for now. + if routine.selByDef == true: + #skip routines that don't have at least one source and one destination. + #list.routineModel.routineSeq() returns an empty seq so len != 0 when empty... + if routine.sources.len != 0 and routine.destinations.len != 0: + routineRuncount += 1 + for source in routine.sources: + for destination in routine.destinations: + list.runStatus = "Bkup " & source & " to " & destination & "..." + #try without requiring superuser privs by default. + rsyncRun = execCmdEx("rsync -aq " & source & " " & destination) + if rsyncRun.exitCode != 0: + #handle permission denied error. + if rsyncRun.exitCode == 23: + let rsyncRunCmd = "SUDO_ASKPASS=" & getAskPassPath() & " sudo -A rsync -aq " & source & " " & destination + rsyncRun = execCmdEx(rsyncRunCmd) + if rsyncRun.exitCode != 0: + rsyncErrors.add("EZ-Bkup's rsync process(es) returned error (" & $rsyncRun.output & ") while attempting to back up " & source & " to " & destination) + else: + rsyncErrors.add("EZ-Bkup's rsync process(es) returned error (" & $rsyncRun.output & ") while attempting to back up " & source & " to " & destination) + #makes the "Bkup Complete" msg below wait on the rsyncRun to finish. + if rsyncRun.exitCode == 1 or rsyncRun.exitCode == 0: + if rsyncErrors.len > 0: + list.runStatus = "Error! Please see the log at ~/.ez-bkup/errors.log" + for err in rsyncErrors: + writeErrorToLog(err) + echo err + elif routineRunCount == 0: + list.runStatus = "Meh. No Bkup Routines were run." + else: + list.runStatus = "Bkup Complete!" + else: + Box {.expand: false.}: + orient = OrientY + margin = 6 + spacing = 12 + Label: + text = "You don't have any Bkup Routines!!! 🙂" + xAlign = 0 + useMarkup = true + Label: + text = "Please click on the + Routine button above to create your first Routine." + xAlign = 0 + useMarkup = true +export RoutineList