FIX: Make it work, then make it clean

main
Kamal Curi 4 years ago
parent f746230394
commit 078593e43c

@ -6,6 +6,7 @@ import sys
import os import os
import pyperclip as pc import pyperclip as pc
import random import random
version = sys.argv[1] version = sys.argv[1]
## Initialization of bottomline dependencies ## Initialization of bottomline dependencies
@ -22,33 +23,57 @@ fields = ["service", "user", "pswd"]
HOMEDIR = os.environ['HOME'] HOMEDIR = os.environ['HOME']
PASFILE=HOMEDIR+"/.pasfile.csv" PASFILE=HOMEDIR+"/.pasfile.csv"
# Initializes Curses' screen
def main(stdscr): def reloadFiles():
# Opens password file files.clear()
with open(PASFILE, mode='r') as pasfile: with open(PASFILE, mode='r') as pasfile:
# Creates reader object # Creates reader object
csvreader=csv.DictReader(pasfile) csvreader=csv.DictReader(pasfile)
for ids in csvreader: for ids in csvreader:
files.append(ids) files.append(ids)
## Initializes color pairs
# Password pallete (Foreground = Background)
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_BLACK) # Initializes the curses screen
pwd_pallete = curses.color_pair(1) stdscr = curses.initscr()
# Main window pallete curses.noecho()
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK) curses.cbreak()
main_pallete = curses.color_pair(2) stdscr.keypad(True)
# Clears screen
stdscr.clear() stdscr.clear()
def steelbox():
reloadFiles()
# Initializes the main window
global mainwin
mainwin = curses.newwin(TERM_LINES -1, TERM_COLS, 0, 0)
mainwin.keypad(True)
mainwin.border()
# Initializes the bottom window
global statuswin
statuswin= curses.newwin(3, TERM_COLS, TERM_LINES, 0)
statuswin.keypad(True)
statuswin.border()
cleanWins()
while True:
reloadFiles()
displayItems()
stdscr.move(TERM_LINES, TERM_COLS)
command()
# Defines global variables
def globals():
## Global variables
# Determines terminal size # Determines terminal size
global TERM_LINES global TERM_LINES
TERM_LINES=curses.LINES - 1 TERM_LINES=curses.LINES - 1
if TERM_LINES <= 20: if TERM_LINES <= 20:
sys.exit("ERROR: Your terminal is too small!") close("ERROR: Your terminal is too small!")
global TERM_COLS global TERM_COLS
TERM_COLS=curses.COLS - 1 TERM_COLS=curses.COLS - 1
if TERM_COLS <=80: if TERM_COLS <=80:
sys.exit("ERROR: Your terminal is too small!") close("ERROR: Your terminal is too small!")
# Global (program-wide) variables for cursor position # Global (program-wide) variables for cursor position
global LINE global LINE
LINE = 0 LINE = 0
@ -89,7 +114,6 @@ def main(stdscr):
# What row the cursor is in # What row the cursor is in
global CUROW global CUROW
CUROW = 0 CUROW = 0
global PSERV global PSERV
PSERV = "" PSERV = ""
global PUSER global PUSER
@ -97,76 +121,17 @@ def main(stdscr):
global PPSWD global PPSWD
PPSWD = "" PPSWD = ""
# Gets user command
def command():
# Initializes the main window
mainwin = curses.newwin(TERM_LINES -1, TERM_COLS, 0, 0)
mainwin.keypad(True)
mainwin.bkgd(' ', main_pallete)
mainwin.border()
# Initializes the bottom window
statusWin = curses.newwin(3, TERM_COLS, TERM_LINES, 0)
statusWin.keypad(True)
statusWin.border()
statusWin.bkgd(' ', main_pallete)
statusWin.border()
while True:
# Creates a name list
displayList = []
# This makes sure the cursor stays on the screen TODO: delete this
if ITEM_CURSOR < 0: ITEM_CURSOR = 0
if ITEM_CURSOR > MAX_ITEMS or ITEM_CURSOR > len(files)-1: ITEM_CURSOR = 0
# Makes sure the windows are properly clean
mainwin.border()
statusWin.border()
mainwin.border()
mainwin.addstr(0, 1, "SteelBox V" + str(version))
mainwin.refresh()
statusWin.border()
statusWin.refresh()
# Reset global necessities
LINE = 0
COLUMN = 0
currItem = 0
highOpt = ()
# Defines what to display
startDisplay = (CURR_PAGE-1)*MAX_ITEMS
stopDisplay = CURR_PAGE*MAX_ITEMS
# Appends the names in the CSV to display on the main window
for ps_name in files:
displayList.append(ps_name['service'][:15])
for item in displayList[startDisplay:stopDisplay]:
# If the item is the one with the cursor, highlight it
if currItem == ITEM_CURSOR:
mode = curses.A_REVERSE
highOpt = mainwin.getyx()
else:
mode = curses.A_NORMAL
mainwin.addstr(1 + LINE, 1 + COLUMN, item, mode)
LINE+=1
currItem+=1
if LINE >= WINLIMIT:
LINE = 0
COLUMN+=16
NROWS+=1
mainwin.refresh()
STATUS_MESSAGE = "PrvPage(F1),NxtPage(F2),(d|el),(e)xamine,(n)ew,(c)opy,(m)odify,(r)andom,(q)uit"
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
## Command logic
# The GLOBAL_CURSOR points to the main files object, so that it gets the right file. # The GLOBAL_CURSOR points to the main files object, so that it gets the right file.
global c global c
global CUROW
global ITEM_CURSOR
global GLOBAL_CURSOR
global CURR_PAGE
c = mainwin.getch() c = mainwin.getch()
if c == ord('q'): if c == ord('q'):
return(0) close()
elif c == curses.KEY_DOWN: elif c == curses.KEY_DOWN:
if GLOBAL_CURSOR < len(files) - 1: if GLOBAL_CURSOR < len(files) - 1:
ITEM_CURSOR+=1 ITEM_CURSOR+=1
@ -176,10 +141,17 @@ def main(stdscr):
ITEM_CURSOR-=1 ITEM_CURSOR-=1
GLOBAL_CURSOR-=1 GLOBAL_CURSOR-=1
elif c == curses.KEY_RIGHT: elif c == curses.KEY_RIGHT:
if CUROW < NROWS: if CUROW < NROWS and (ITEM_CURSOR + MAX_LINES) < len(files) - 1:
ITEM_CURSOR+=MAX_LINES - 1 ITEM_CURSOR+=MAX_LINES - 1
GLOBAL_CURSOR+=MAX_LINES - 1 GLOBAL_CURSOR+=MAX_LINES - 1
CUROW+=1 CUROW+=1
else:
ITEM_CURSOR = len(files) - 1
GLOBAL_CURSOR = len(files) - 1
if CUROW < NROWS:
CUROW+=1
else:
CUROW = NROWS
elif c == curses.KEY_LEFT: elif c == curses.KEY_LEFT:
if CUROW > 0: if CUROW > 0:
ITEM_CURSOR-=MAX_LINES - 1 ITEM_CURSOR-=MAX_LINES - 1
@ -194,49 +166,64 @@ def main(stdscr):
if CURR_PAGE < MAX_PAGES: if CURR_PAGE < MAX_PAGES:
CURR_PAGE+=1 CURR_PAGE+=1
GLOBAL_CURSOR+=MAX_ITEMS GLOBAL_CURSOR+=MAX_ITEMS
elif c == 10 or c == curses.KEY_ENTER or c == ord('e'):
examine()
elif c == ord('c') or c == curses.KEY_F3: elif c == ord('c') or c == curses.KEY_F3:
if len(files) > 0: copy()
pc.copy(files[GLOBAL_CURSOR]['pswd']) elif c == ord('n'):
statusWin.border() newFile()
STATUS_MESSAGE = "Copied password for " + files[GLOBAL_CURSOR]['service'] elif c == ord('m'):
statusWin.addstr(0,0, STATUS_MESSAGE) modFile()
statusWin.refresh() elif c == ord('d') or c == curses.KEY_DC:
mainwin.getch() delFile()
elif c == ord('r'): elif c == ord('r'):
ranWin = curses.newwin(3, 49, int(TERM_LINES/2), int(TERM_COLS/2)) rwin()
ranWin.border()
ranWin.addstr(0, 1, "Random string")
ranWin.addstr(1, 1, randString())
ranWin.refresh()
ranWin.getch()
elif c == ord('d') or c == curses.KEY_DC:
dlWin = curses.newwin(3, 22, int(TERM_LINES/2), int(TERM_COLS/2)) def newFile():
dlWin.border() # Initializes the 'new password' window
dlWin.refresh() npWin = curses.newwin(5, 60,int(TERM_LINES/2)-2, int(TERM_COLS/2)-18)
statusWin.border() nwCord = npWin.getbegyx()
STATUS_MESSAGE = "Delete " + displayList[GLOBAL_CURSOR] + "?" # Initializes the windows in which the textboxes will reside for input
statusWin.addstr(0,0, STATUS_MESSAGE) svWin = curses.newwin(1, 45, nwCord[0]+1, nwCord[1]+6)
statusWin.refresh() svBox = Textbox(svWin)
dlWin.addstr(1, 1, "Are you sure? (y/N)") usWin = curses.newwin(1, 45, nwCord[0]+2, nwCord[1]+6)
c = dlWin.getch() usBox = Textbox(usWin)
if c == ord('y'): psWin = curses.newwin(1, 45, nwCord[0]+3, nwCord[1]+6)
files.pop(GLOBAL_CURSOR) psBox = Textbox(psWin)
# Clears the 'new password' window
npWin.border()
npWin.border()
npWin.addstr(0, 1, "New password")
npWin.addstr(1, 1, "SRVC:")
npWin.addstr(2, 1, "USER:")
npWin.addstr(3, 1, "PSWD:")
STATUS_MESSAGE = "CTRL+G to enter, MAX 45 CHARS"
displayStatus(STATUS_MESSAGE)
npWin.refresh()
# Takes data
svBox.edit()
passService = svBox.gather()
usBox.edit()
passUser = usBox.gather()
psBox.edit()
passPswd = psBox.gather()
if passService != '' and passUser != '':
if passPswd == '':
passPswd = randString()
# wtf = write to file
wtf = {'service' : passService, 'user' : passUser, 'pswd' : passPswd}
files.append(wtf)
with open(PASFILE, mode='w') as pasfile: with open(PASFILE, mode='w') as pasfile:
csvwriter = csv.DictWriter(pasfile, fields) csvwriter = csv.DictWriter(pasfile, fields)
csvwriter.writeheader() csvwriter.writeheader()
csvwriter.writerows(files) csvwriter.writerows(files)
files.clear() reloadFiles()
with open(PASFILE, mode='r') as pasfile:
# Creates reader object
csvreader=csv.DictReader(pasfile)
for ids in csvreader:
files.append(ids)
# For some reason, KEY_UP is 10, instead of the 343 the debbuger flags... Welp ¯\_(ツ)_/¯
elif c == 10 or c == curses.KEY_ENTER or c == ord('e'): def examine():
# highOpt = Coordinates of the first option's character # highOpt = Coordinates of the first option's character
LINE = highOpt[0] LINE = highOpt[0]
COLUMN = highOpt[1] COLUMN = highOpt[1]
@ -249,8 +236,7 @@ def main(stdscr):
fileWin = curses.newwin(5, 60, LINE+5, COLUMN) fileWin = curses.newwin(5, 60, LINE+5, COLUMN)
# Clears the window # Clears the window
fileWin.border() fileWin.border()
fileWin.border() fileWin.border
passService = files[GLOBAL_CURSOR]['service'][:45] passService = files[GLOBAL_CURSOR]['service'][:45]
passUser = files[GLOBAL_CURSOR]['user'][:45] passUser = files[GLOBAL_CURSOR]['user'][:45]
passPswd = files[GLOBAL_CURSOR]['pswd'][:45] passPswd = files[GLOBAL_CURSOR]['pswd'][:45]
@ -258,36 +244,18 @@ def main(stdscr):
fileWin.addstr(1, 1, "SRVC: " + passService) fileWin.addstr(1, 1, "SRVC: " + passService)
fileWin.addstr(2, 1, "NAME: " + passUser) fileWin.addstr(2, 1, "NAME: " + passUser)
fileWin.addstr(3, 1, "PSWD: " + passPswd) fileWin.addstr(3, 1, "PSWD: " + passPswd)
statusWin.border()
STATUS_MESSAGE = "cmds:(d|DEL)ete,(m)odify, (c)opy " STATUS_MESSAGE = "cmds:(d|DEL)ete,(m)odify, (c)opy "
statusWin.addstr(0,0, STATUS_MESSAGE) displayStatus(STATUS_MESSAGE)
statusWin.refresh()
fileWin.refresh()
# Gets command to act on the highlighted file # Gets command to act on the highlighted file
c = fileWin.getch() c = fileWin.getch()
if c == ord('d') or c == curses.KEY_DC: if c == ord('d') or c == curses.KEY_DC:
dlWin = curses.newwin(3, 22, int(TERM_LINES/2), int(TERM_COLS/2)) delFile()
dlWin.border()
dlWin.refresh()
statusWin.border()
STATUS_MESSAGE = "Delete " + displayList[GLOBAL_CURSOR] + "?"
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
dlWin.addstr(1, 1, "Are you sure? (y/N)")
c = dlWin.getch()
if c == ord('y'):
files.pop(GLOBAL_CURSOR)
with open(PASFILE, mode='w') as pasfile:
csvwriter = csv.DictWriter(pasfile, fields)
csvwriter.writeheader()
csvwriter.writerows(files)
files.clear()
with open(PASFILE, mode='r') as pasfile:
# Creates reader object
csvreader=csv.DictReader(pasfile)
for ids in csvreader:
files.append(ids)
elif c == ord('m'): elif c == ord('m'):
modFile()
elif c == ord('c') or c == curses.KEY_F3:
copy()
def modFile():
# Extracts the file to be modified # Extracts the file to be modified
modFile = files[GLOBAL_CURSOR] modFile = files[GLOBAL_CURSOR]
# Removes it from the main file # Removes it from the main file
@ -312,8 +280,7 @@ def main(stdscr):
# Clears the 'modify password' window # Clears the 'modify password' window
modWin.border() modWin.border()
modWin.border() modWin.border
modWin.addstr(0, 1, "Modify password") modWin.addstr(0, 1, "Modify password")
modWin.addstr(1, 1, "SRVC:") modWin.addstr(1, 1, "SRVC:")
modWin.addstr(2, 1, "USER:") modWin.addstr(2, 1, "USER:")
@ -325,21 +292,15 @@ def main(stdscr):
# Takes data # Takes data
STATUS_MESSAGE = "Edit SERVICE field - CTRL+G to enter, leave empty to cancel, MAX 45 CHARS" STATUS_MESSAGE = "Edit SERVICE field - CTRL+G to enter, leave empty to cancel, MAX 45 CHARS"
statusWin.border() displayStatus(STATUS_MESSAGE)
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
svBox.edit() svBox.edit()
passService = svBox.gather() passService = svBox.gather()
STATUS_MESSAGE = "Edit USER field - CTRL+G to enter, leave empty to cancel, MAX 45 CHARS" STATUS_MESSAGE = "Edit USER field - CTRL+G to enter, leave empty to cancel, MAX 45 CHARS"
statusWin.border() displayStatus(STATUS_MESSAGE)
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
usBox.edit() usBox.edit()
passUser = usBox.gather() passUser = usBox.gather()
STATUS_MESSAGE = "Edit PASSWORD field - CTRL+G to enter, leave empty for random string" STATUS_MESSAGE = "Edit PASSWORD field - CTRL+G to enter, leave empty for random string"
statusWin.border() displayStatus(STATUS_MESSAGE)
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
psBox.edit() psBox.edit()
passPswd = psBox.gather() passPswd = psBox.gather()
modFile = {'service' : passService, 'user' : passUser, 'pswd' : passPswd} modFile = {'service' : passService, 'user' : passUser, 'pswd' : passPswd}
@ -349,137 +310,114 @@ def main(stdscr):
csvwriter = csv.DictWriter(pasfile, fields) csvwriter = csv.DictWriter(pasfile, fields)
csvwriter.writeheader() csvwriter.writeheader()
csvwriter.writerows(files) csvwriter.writerows(files)
files.clear() reloadFiles()
with open(PASFILE, mode='r') as pasfile:
# Creates reader object
csvreader=csv.DictReader(pasfile)
for ids in csvreader:
files.append(ids)
elif c == ord('c') or c == curses.KEY_F3:
def delFile():
dlWin = curses.newwin(3, 22, int(TERM_LINES/2), int(TERM_COLS/2))
dlWin.border()
dlWin.refresh()
STATUS_MESSAGE = "Delete " + displayList[GLOBAL_CURSOR] + "?"
displayStatus(STATUS_MESSAGE)
dlWin.addstr(1, 1, "Are you sure? (y/N)")
c = dlWin.getch()
if c == ord('y'):
files.pop(GLOBAL_CURSOR)
with open(PASFILE, mode='w') as pasfile:
csvwriter = csv.DictWriter(pasfile, fields)
csvwriter.writeheader()
csvwriter.writerows(files)
reloadFiles()
# Opens a new window with a random string of length 45
def rwin():
ranWin = curses.newwin(3, 49, int(TERM_LINES/2), int(TERM_COLS/2))
ranWin.border()
ranWin.addstr(0, 1, "Random string")
ranWin.addstr(1, 1, randString())
ranWin.refresh()
ranWin.getch()
# Copies password to clipboard
def copy():
if len(files) > 0:
pc.copy(files[GLOBAL_CURSOR]['pswd']) pc.copy(files[GLOBAL_CURSOR]['pswd'])
statusWin.border()
STATUS_MESSAGE = "Copied password for " + files[GLOBAL_CURSOR]['service'] STATUS_MESSAGE = "Copied password for " + files[GLOBAL_CURSOR]['service']
statusWin.addstr(0,0, STATUS_MESSAGE) displayStatus(STATUS_MESSAGE)
statusWin.refresh()
mainwin.getch() mainwin.getch()
elif c == ord('n'):
# Initializes the 'new password' window
npWin = curses.newwin(5, 60,int(TERM_LINES/2)-2, int(TERM_COLS/2)-18)
nwCord = npWin.getbegyx()
# Initializes the windows in which the textboxes will reside for input
svWin = curses.newwin(1, 45, nwCord[0]+1, nwCord[1]+6)
svBox = Textbox(svWin)
usWin = curses.newwin(1, 45, nwCord[0]+2, nwCord[1]+6)
usBox = Textbox(usWin)
psWin = curses.newwin(1, 45, nwCord[0]+3, nwCord[1]+6)
psBox = Textbox(psWin)
# Clears the 'new password' window # Cleans the windows
npWin.border() def cleanWins():
npWin.border() mainwin.clear()
statuswin.clear()
mainwin.border()
statuswin.border()
mainwin.addstr(0, 1, "SteelBox V" + str(version))
mainwin.refresh()
statuswin.refresh()
npWin.addstr(0, 1, "New password")
npWin.addstr(1, 1, "SRVC:")
npWin.addstr(2, 1, "USER:")
npWin.addstr(3, 1, "PSWD:")
STATUS_MESSAGE = "CTRL+G to enter, MAX 45 CHARS"
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
npWin.refresh()
# Takes data
svBox.edit()
passService = svBox.gather()
usBox.edit()
passUser = usBox.gather()
psBox.edit()
passPswd = psBox.gather()
if passService != '' and passUser != '': # Displays the items on the screen properly
if passPswd == '': def displayItems():
passPswd = randString() cleanWins()
# wtf = write to file global NROWS
wtf = {'service' : passService, 'user' : passUser, 'pswd' : passPswd} global ITEM_CURSOR
files.append(wtf) # Creates a name list
with open(PASFILE, mode='w') as pasfile: global displayList
csvwriter = csv.DictWriter(pasfile, fields) displayList = []
csvwriter.writeheader() # Appends the names in the CSV to display on the main window
csvwriter.writerows(files) for ps_name in files:
displayList.append(ps_name['service'][:15])
elif c == ord('m'):
# Extracts the file to be modified
modFile = files[GLOBAL_CURSOR]
# Removes it from the main file
files.pop(GLOBAL_CURSOR)
# Creates the 'modify password' window
modWin = curses.newwin(5, 60,int(TERM_LINES/2)-2, int(TERM_COLS/2)-18)
# Gets the coordinates for the top left corner of said window
nwCord = modWin.getbegyx()
# Creates the fields in which the password will be edited
svWin = curses.newwin(1, 45, nwCord[0]+1, nwCord[1]+6)
svWin.addstr(0, 0, modFile['service'])
svWin.move(0, 0)
svBox = Textbox(svWin)
usWin = curses.newwin(1, 45, nwCord[0]+2, nwCord[1]+6)
usWin.addstr(0, 0, modFile['user'])
usWin.move(0, 0)
usBox = Textbox(usWin)
psWin = curses.newwin(1, 45, nwCord[0]+3, nwCord[1]+6)
psWin.addstr(0, 0, modFile['pswd'])
psWin.move(0, 0)
psBox = Textbox(psWin)
# Clears the 'modify password' window
modWin.border()
modWin.border()
modWin.addstr(0, 1, "Modify password") # Reset global necessities
modWin.addstr(1, 1, "SRVC:") LINE = 0
modWin.addstr(2, 1, "USER:") COLUMN = 0
modWin.addstr(3, 1, "PSWD:") currItem = 0
STATUS_MESSAGE = "CTRL+G to enter, MAX 45 CHARS" NROWS = 0
statusWin.addstr(0,0, STATUS_MESSAGE) global highOpt
statusWin.refresh() highOpt = ()
modWin.refresh() # Defines what to display
svWin.refresh() startDisplay = (CURR_PAGE-1)*MAX_ITEMS
usWin.refresh() stopDisplay = CURR_PAGE*MAX_ITEMS
psWin.refresh()
for item in displayList[startDisplay:stopDisplay]:
# If the item is the one with the cursor, highlight it
if currItem == ITEM_CURSOR:
mode = curses.A_REVERSE
highOpt = mainwin.getyx()
else:
mode = curses.A_NORMAL
mainwin.addstr(1 + LINE, 1 + COLUMN, item, mode)
LINE+=1
currItem+=1
if LINE >= WINLIMIT:
LINE = 0
COLUMN+=16
NROWS+=1
STATUS_MESSAGE = "PrvPage(F1),NxtPage(F2),(d|el),(e)xamine,(n)ew,(c)opy,(m)odify,(r)andom,(q)uit"
displayStatus(STATUS_MESSAGE)
mainwin.refresh()
# Displays on the status window
def displayStatus(msg):
statuswin.border()
statuswin.addstr(0,0, msg)
statuswin.refresh()
# Takes data
STATUS_MESSAGE = "Edit SERVICE field"
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
svBox.edit()
passService = svBox.gather()
STATUS_MESSAGE = "Edit USER field"
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
usBox.edit()
passUser = usBox.gather()
STATUS_MESSAGE = "Edit PASSWORD field"
statusWin.addstr(0,0, STATUS_MESSAGE)
statusWin.refresh()
psBox.edit()
passPswd = psBox.gather()
modFile = {'service' : passService, 'user' : passUser, 'pswd' : passPswd}
files.insert(GLOBAL_CURSOR, modFile)
with open(PASFILE, mode='w') as pasfile:
# Creates writer object and writes to the csv file
csvwriter = csv.DictWriter(pasfile, fields)
csvwriter.writeheader()
csvwriter.writerows(files)
files.clear()
with open(PASFILE, mode='r') as pasfile:
# Creates reader object
csvreader=csv.DictReader(pasfile)
for ids in csvreader:
files.append(ids)
# Returns a random string of length 45
def randString(): def randString():
result = '' result = ''
for _ in range(45): for _ in range(45):
@ -489,4 +427,20 @@ def randString():
result += chr(ascNum) result += chr(ascNum)
return(result) return(result)
wrapper(main)
# Finishes the application
def close(error = ''):
curses.nocbreak()
stdscr.keypad(False)
curses.echo()
curses.endwin()
if error == '':
print("Goodbye!")
sys.exit(error)
globals()
steelbox()
close()
Loading…
Cancel
Save