initial commit

master
itwrx 1 year ago
commit 188d6d1a3e
  1. 10
      CHANGELOG.md
  2. 69
      ez_bkup.nim
  3. BIN
      icons.tar.gz
  4. BIN
      icons/EZ-Bkup-icon.png
  5. 45
      icons/entity-edit-dark-theme.svg
  6. 1
      icons/entity-edit.svg
  7. 187
      models/routine.nim
  8. 83
      shared.nim
  9. 5
      styles.css
  10. 42
      views/app_menu_button.nim
  11. 41
      views/edit_routine_dialog.nim
  12. 221
      views/routine_editor.nim
  13. 219
      views/routine_list.nim

@ -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)

@ -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 <https://www.gnu.org/licenses/> 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 = "<span size=\"x-large\">Bkup Routines</span>"
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")])

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg4"
sodipodi:docname="entity-edit.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.6640625"
inkscape:cx="256"
inkscape:cy="256"
inkscape:window-width="1920"
inkscape:window-height="1012"
inkscape:window-x="1920"
inkscape:window-y="44"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path
d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"
id="path2" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.639399"
d="M 20.131455,511.30159 C 12.633018,510.11175 5.9356881,504.94557 2.5273571,497.72214 0.80908211,494.08053 0.61907627,493.12506 0.64628672,488.26291 0.6759534,482.96188 1.0418939,481.61881 19.071696,420.63829 29.189024,386.4194 38.315718,356.26729 39.35324,353.6336 c 2.635465,-6.68998 7.229742,-14.82621 11.729142,-20.77171 2.512161,-3.31956 44.396616,-45.61057 122.247588,-123.4342 L 291.75609,91.043174 356.35998,155.64706 420.96387,220.25095 300.92325,340.17902 C 171.1693,469.81131 178.1297,463.17622 165.19474,469.56391 c -2.93974,1.45173 -7.33514,3.35072 -9.76757,4.21997 -7.84741,2.80437 -126.489492,37.44519 -129.286325,37.74871 -1.487324,0.16141 -4.191549,0.0574 -6.00939,-0.231 z"
id="path183" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.639399"
d="M 379.49299,132.50707 314.89566,67.907648 341.03468,41.748365 c 17.00808,-17.021254 27.66441,-27.217667 30.50572,-29.189106 15.62214,-10.8394197 34.50594,-14.2580075 52.72251,-9.5445076 6.15681,1.5930596 16.91628,6.8355343 21.93427,10.6873156 2.14836,1.649065 14.6086,13.775928 27.68942,26.948586 25.38314,25.561343 27.63744,28.252973 31.83856,38.015094 4.0333,9.372178 5.06587,14.641025 5.01643,25.597163 -0.0377,8.36936 -0.24566,10.37066 -1.60466,15.44762 -1.90565,7.11912 -5.3524,14.6212 -9.39319,20.44494 -1.97605,2.84794 -12.19763,13.52857 -29.3429,30.66069 l -26.31052,26.29034 z"
id="path185" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/></svg>

After

Width:  |  Height:  |  Size: 551 B

@ -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()

@ -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 <https://www.gnu.org/licenses/> 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

@ -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;}

@ -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 <https://www.gnu.org/licenses/> 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

@ -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 <https://www.gnu.org/licenses/> 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

@ -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 <https://www.gnu.org/licenses/> 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 = "<span weight=\"bold\">Name:</span>"
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 = "<span weight=\"bold\">Enabled?</span>"
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 = "<span weight=\"bold\">Sources:</span>"
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 = "<span weight=\"bold\">Destinations:</span>"
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

@ -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 <https://www.gnu.org/licenses/> 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 = "<span size=\"large\" weight=\"bold\">" & routine.name & "</span>"
text = "<span size=\"large\">" & routine.name & "</span>"
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 = "<span color=\"#6fffa3\">Sources:</span>"
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 = "<span color=\"#ff6b6b\">Routine will be ignored. Source required.</span>"
xAlign = 0
useMarkup = true
Label:
text = "<span color=\"#6fffa3\">Destinations:</span>"
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 = "<span color=\"#ff6b6b\">Routine will be ignored. Destination required.</span>"
xAlign = 0
useMarkup = true
Box {.expand: false.}:
orient = OrientX
spacing = 6
margin = 6
Label {.expand: false.}:
text = "<span size=\"large\">Status: </span>"
xAlign = 0
useMarkup = true
Label:
if list.runStatus != "":
text = list.runStatus
else:
text = "<span color=\"#6AC9FF\" size=\"large\">Waiting patiently...</span>"
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 = "<span size=\"large\">Run Bkup!</span>"
xAlign = 0
useMarkup = true
proc clicked() =
list.runStatus = "<span color=\"#FFE97B\" size=\"large\">Running Bkup...</span>"
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 = "<span color=\"#FFE97B\" size=\"large\">Bkup " & source & " to " & destination & "...</span>"
#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 = "<span color=\"#ff6b6b\" size=\"large\">Error! Please see the log at ~/.ez-bkup/errors.log</span>"
for err in rsyncErrors:
writeErrorToLog(err)
echo err
elif routineRunCount == 0:
list.runStatus = "<span color=\"#FFA651\" size=\"large\">Meh. No Bkup Routines were run.</span>"
else:
list.runStatus = "<span color=\"#6fffa3\" size=\"large\">Bkup Complete!</span>"
else:
Box {.expand: false.}:
orient = OrientY
margin = 6
spacing = 12
Label:
text = "<span size=\"large\">You don't have any Bkup Routines!!! 🙂</span>"
xAlign = 0
useMarkup = true
Label:
text = "<span size=\"large\">Please click on the + Routine button above to create your first Routine.</span>"
xAlign = 0
useMarkup = true
export RoutineList
Loading…
Cancel
Save