├── README ├── Working_Copy_Sync.py ├── Working_Copy_Sync.pyui ├── popoverButton.pyui ├── popoverInputAlert.pyui ├── rxFile.py └── rxZip.py /README: -------------------------------------------------------------------------------- 1 | 1. Introduction 2 | 3 | This is a project for making working with github in pythonista easier. It uses x-callback-url to get repository data from github through the app "Working Copy". I am not affiliated with Working Copy in any way. I will not benefit from you using it, and make no promises about the quality of the software. This code is also provided without guarantee, and you should test it before trusting your code to it. 4 | 5 | This code was based on wc_sync by pysmath, https://github.com/pysmath. 6 | 7 | 2. Setup 8 | 9 | Before using this you need to have a remote repository set up, and of course have working copy installed and hooked up to your github (or other service) account. Then you can either download a repository, or initialize a new one and hook it up to one online by specifying the remote url. 10 | 11 | Finally, you need to enable x-callback-url in the working copy settings. Now you're ready to work in pythonista. 12 | 13 | The first time you run the code it will ask you for the Working Copy key. This will be stored in the Pythonista keychain so you will not need to enter it again. 14 | 15 | You will also have to set INSTALL_PATH to match the path where you have installed rxFile.py and rxZip.py. This should not have a leading or trailing '/'. 16 | 17 | 3. Instructions 18 | 19 | The bookmark icon will open Working Copy. 20 | 21 | The copy icon will clone a respository within Working Copy into Pythonista. A directory with the same name as the repository will be created in the root of the Pythonista file system. If the files already exist they will be overwritten, you will be warned if the top level directory already exists. 22 | 23 | The download icon will download the version of the file from Working Copy and overwrite the version in Pythonista. The file will be downloaded from the repository with the same name as the top level directory within the Pythonista file system. 24 | 25 | The upload icon will write the latest script to the Working Copy version of the file. If the file is new it will be added in working copy as a new file in the repository. The file will be written into the repository with the same name as the top level directory within the Pythonista file system, any intermediate directories will be created in the Working Copy repository. 26 | 27 | The filmstrip icon will upload the pyui file relating to the python script to Working Copy. The file will be written into the repository with the same name as the top level directory within the Pythonista file system, any intermediate directories will be created in the Working Copy repository 28 | 29 | 4. Known Issues 30 | 31 | Requires Working Copy 1.5 or better. 32 | 33 | Local files have to have their top level directory (repository name) in the root of the Pythonista file system. -------------------------------------------------------------------------------- /Working_Copy_Sync.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import ui 4 | import editor 5 | import os 6 | import console 7 | import webbrowser as wb 8 | import urllib 9 | import base64 10 | import time 11 | import keychain 12 | 13 | #This should not have leading or trailing / 14 | INSTALL_PATH = 'wc_sync' 15 | 16 | #Key set in checkKey funtion 17 | key = None 18 | 19 | def showPopupInputAlert(title, handler, text, yPos): 20 | v = ui.load_view('popoverInputAlert') 21 | v['label'].text = title 22 | v['buttonOK'].action = handler 23 | xPos = 990 24 | yPos = yPos + 84 25 | v['textfield'].text = text 26 | v['textfield'].begin_editing() 27 | v.height = 90 28 | v.present('popover', popover_location=(xPos,yPos), hide_title_bar=True) 29 | 30 | def getPopupText(sender): 31 | text = None 32 | for v in sender.superview.subviews: 33 | if v.name =='textfield': 34 | text = v.text 35 | sender.superview.close() 36 | return text 37 | 38 | def showPopupButton(title, handler, yPos): 39 | v = ui.load_view('popoverButton') 40 | v['button'].title = title 41 | v['button'].action = handler 42 | xPos = 990 43 | yPos = yPos + 75 44 | v.height = 6 45 | v.present('popover', popover_location=(xPos,yPos), hide_title_bar=True) 46 | 47 | def closePopup(sender): 48 | sender.superview.close() 49 | 50 | def info(): 51 | documentsDir = os.path.expanduser('~/Documents') 52 | info = editor.get_path() 53 | #documentsDir starts with '/private' whereas info does not 54 | fullPath = info[len(documentsDir)-7:] 55 | path = fullPath.split('/',1)[1] 56 | repo = fullPath.split('/',1)[0] 57 | return repo,path 58 | 59 | def sendB64(repo,path,text): 60 | url = 'working-copy://x-callback-url/write/?' 61 | b64 = base64.b64encode(text) 62 | f = {'repo':repo,'path':path,'key':key,'base64':b64,'x-success':'pythonista://'} 63 | url += urllib.urlencode(f).replace('+','%20') 64 | wb.open(url) 65 | 66 | def sendText(repo,path,text): 67 | url = 'working-copy://x-callback-url/write/?' 68 | f = {'repo':repo,'path':path,'key':key,'text':text,'x-success':'pythonista://'} 69 | url += urllib.urlencode(f).replace('+','%20') 70 | wb.open(url) 71 | 72 | def open_wc(sender): # Opens working copy 73 | wb.open('working-copy://') 74 | 75 | @ui.in_background 76 | def copyFromWCPt1(sender): 77 | showPopupButton('Pull', copyFromWCPt2, sender.y) 78 | 79 | def copyFromWCPt2(sender): # Copies the text from the working copy version of the file and uses it to overwrite the contents of the corresponding file in pythonista. 80 | closePopup(sender) 81 | repo,path = info() 82 | url = 'working-copy://x-callback-url/read/?' 83 | success = 'pythonista://'+INSTALL_PATH+'/rxFile.py?action=run&argv=' + os.path.join(repo,path) +'&argv=' 84 | f = {'repo':repo,'path':path,'key':key, 'base64':'1'} 85 | url += urllib.urlencode(f).replace('+','%20') 86 | url += '&x-success=' + urllib.quote_plus(success) 87 | wb.open(url) 88 | 89 | def sendToWCPt1(sender): 90 | showPopupButton('Push', sendToWCPt2, sender.y) 91 | 92 | def sendToWCPt2(sender): # Sends the contents of the file in pythonista to overwrite the working copy version. 93 | closePopup(sender) 94 | repo,path = info() 95 | sendText(repo,path,editor.get_text()) 96 | 97 | def sendPYUIToWCPt1(sender): 98 | showPopupButton('Push .pyui', sendPYUIToWCPt2, sender.y) 99 | 100 | def sendPYUIToWCPt2(sender): 101 | closePopup(sender) 102 | repo,path = info() 103 | path += 'ui' 104 | fullPath = os.path.join(os.path.expanduser('~/Documents'), os.path.join(repo,path)) 105 | with open(fullPath) as file: 106 | sendB64(repo,path,file.read()) 107 | 108 | def getZipPt1(sender): 109 | showPopupInputAlert('Repository', getZipPt2, "", sender.y) 110 | 111 | def getZipPt2(sender): 112 | repo = getPopupText(sender) 113 | if len(repo) > 0: 114 | url = 'working-copy://x-callback-url/zip/?' 115 | f = {'repo':repo, 'key':key} 116 | success ='pythonista://'+INSTALL_PATH+'/rxZip.py?action=run&argv='+repo+'&argv=' 117 | url += urllib.urlencode(f).replace('+','%20') 118 | url += '&x-success=' + urllib.quote_plus(success) 119 | wb.open(url) 120 | 121 | def checkKey(): 122 | global key 123 | key = keychain.get_password('wcSync','xcallback') 124 | if key == None: 125 | pwd = console.password_alert('Working Copy Key') 126 | keychain.set_password('wcSync','xcallback',pwd) 127 | 128 | def main(): 129 | checkKey() 130 | view = ui.load_view('Working_Copy_Sync') 131 | view.present('sidebar') 132 | 133 | #psuedo main() function 134 | if __name__ == "__main__": 135 | main() -------------------------------------------------------------------------------- /Working_Copy_Sync.pyui: -------------------------------------------------------------------------------- 1 | [{"class":"View","attributes":{"tint_color":"RGBA(0.000000,0.478000,1.000000,1.000000)","enabled":true,"flex":"","name":"","border_width":1,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","background_color":"RGBA(1.000000,1.000000,1.000000,1.000000)"},"frame":"{{0, 0}, {40, 318}}","nodes":[{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":false,"name":"button1","uuid":"2F36CD19-281A-4F86-986D-486B5036FB54","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","action":"open_wc","image_name":"ionicons-ios7-bookmarks-outline-32","title":""},"frame":"{{0, 32}, {40, 40}}","nodes":[]},{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":false,"name":"button3","uuid":"259744AD-6617-40E9-86E2-6EA4C8B9EF2D","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","action":"getZipPt1","image_name":"ionicons-ios7-copy-outline-32","title":""},"frame":"{{0, 92}, {40, 40}}","nodes":[]},{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":false,"name":"button2","uuid":"D15A4B5E-FBE3-4416-A091-AE62E5D34B74","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","action":"copyFromWCPt1","image_name":"ionicons-ios7-download-outline-32","title":""},"frame":"{{0, 152}, {40, 40}}","nodes":[]},{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":false,"name":"button4","uuid":"1DA51882-FF86-4852-A5EF-4A31A5932D23","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","action":"sendToWCPt1","image_name":"ionicons-ios7-upload-outline-32","title":""},"frame":"{{0, 212}, {40, 40}}","nodes":[]},{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":false,"name":"button5","uuid":"E21AA15B-115B-4A88-B395-89549220667C","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","action":"sendPYUIToWCPt1","image_name":"ionicons-ios7-film-outline-32","title":""},"frame":"{{0, 272}, {40, 40}}","nodes":[]}]}] -------------------------------------------------------------------------------- /popoverButton.pyui: -------------------------------------------------------------------------------- 1 | [{"class":"View","attributes":{"background_color":"RGBA(1.000000,1.000000,1.000000,1.000000)","tint_color":"RGBA(0.000000,0.478000,1.000000,1.000000)","enabled":true,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","flex":""},"frame":"{{0, 0}, {92, 44}}","nodes":[{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":false,"name":"button","uuid":"CEE52015-FD39-477A-B045-17288B5320DB","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","title":"Button"},"frame":"{{6, 6}, {80, 32}}","nodes":[]}]}] -------------------------------------------------------------------------------- /popoverInputAlert.pyui: -------------------------------------------------------------------------------- 1 | [{"class":"View","attributes":{"background_color":"RGBA(1.000000,1.000000,1.000000,1.000000)","tint_color":"RGBA(0.000000,0.478000,1.000000,1.000000)","enabled":true,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","flex":""},"frame":"{{0, 0}, {180, 128}}","nodes":[{"class":"Label","attributes":{"font_size":17,"enabled":true,"text":"Label","flex":"","name":"label","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","text_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","alignment":"center","uuid":"900B3FA1-ED2A-44BB-85BC-8CED0AF1FEFD"},"frame":"{{6, 6.5}, {167.5, 36.5}}","nodes":[]},{"class":"TextField","attributes":{"font_size":17,"enabled":true,"flex":"","name":"textfield","border_style":3,"text_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","alignment":"left","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","uuid":"90FDD44F-79E2-480A-B16F-3FB6D08328A4"},"frame":"{{6, 51}, {167.5, 32}}","nodes":[]},{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":false,"name":"buttonOK","uuid":"E91A856B-8C8A-41D5-BB9E-B3E6327CDE8C","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","title":"OK"},"frame":"{{49, 91}, {80, 31}}","nodes":[]}]}] -------------------------------------------------------------------------------- /rxFile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import editor 5 | import os 6 | import errno 7 | import base64 8 | 9 | path = sys.argv[1] #The path sent through with the x-callback-url. 10 | text = base64.b64decode(sys.argv[2]) # The text from the file 11 | 12 | #Create directory structure if missing 13 | try: 14 | os.makedirs(os.path.join(os.path.expanduser('~/Documents'), path)) 15 | except OSError, e: 16 | #only pass if directory exists 17 | if e.errno != errno.EEXIST: 18 | raise e 19 | pass 20 | 21 | fullPath = os.path.join(os.path.expanduser('~/Documents'), path) 22 | with open(fullPath, 'w') as f: # To clear the file. 23 | f.write(text) # Write the new code to the file. 24 | 25 | #To refresh contents in Pythonista 26 | editor.open_file(path) 27 | console.hud_alert(path +' Updated') 28 | -------------------------------------------------------------------------------- /rxZip.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import base64 3 | import os 4 | import console 5 | import sys 6 | import errno 7 | 8 | ZIP_FILE = 'wc_sync/repo.zip' 9 | 10 | path = sys.argv[1] 11 | 12 | try: 13 | os.makedirs(os.path.join(os.path.expanduser('~/Documents'), path)) 14 | except OSError, e: 15 | if e.errno != errno.EEXIST: 16 | raise e 17 | console.alert('Overwriting existing directory',button1='Continue') 18 | 19 | zipF = os.path.join(os.path.expanduser('~/Documents'), ZIP_FILE) 20 | with open(zipF, 'w') as zip: 21 | zip.write(base64.b64decode(sys.argv[2])) 22 | 23 | z = zipfile.ZipFile(zipF) 24 | z.extractall(os.path.join(os.path.expanduser('~/Documents'), path)) 25 | os.remove(zipF) 26 | console.hud_alert(path + ' Downloaded') 27 | 28 | --------------------------------------------------------------------------------