"""
This file is part of the Ventoo program.
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this software. If not, see .
Copyright 2010 Christopher Harvey
"""
import sys
sys.path.append('../backend')
import augeas
import os.path as osp
import pygtk
pygtk.require('2.0')
import gtk
import augeas_utils
import AugFileTree
import VentooModule
import AugEditTree
import shutil
import os
import re
import ErrorDialog
import gtkmozembed
import difflib
from pygments import highlight
from pygments.lexers import DiffLexer
from pygments.formatters import HtmlFormatter
sandboxDir = '/'
class MainWindow(gtk.Window):
def __init__(self, augeas):
self.a = augeas
#setup the gui
gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
self.connect("delete_event", self.close)
self.docBox = gtk.VPaned()
self.rootBox = gtk.VBox()
self.docWindow = gtkmozembed.MozEmbed()
self.docBox.add2(self.docWindow)
self.docWindow.load_url("about:blank")
self.mainToolbar = gtk.Toolbar()
self.diffButton = gtk.ToolButton(None, "Diff!")
self.diffButton.connect("clicked", self.diffPressed, None)
self.showErrorsButton = gtk.ToolButton(None, "Augeas Errors")
self.showErrorsButton.connect("clicked", self.showErrPressed, None)
self.mainToolbar.insert(self.diffButton, -1)
self.mainToolbar.insert(self.showErrorsButton, -1)
self.rootBox.pack_start(self.mainToolbar, False, False)
self.mainPaned = gtk.HPaned()
self.rootBox.pack_start(self.docBox)
self.files_tv = AugFileTree.AugFileTree()
self.edit_tv = AugEditTree.AugEditTree()
self.edit_tv.connect("cursor-changed", self.nodeChanged, None)
self.files_tv_scrolled_window = gtk.ScrolledWindow()
self.files_tv_scrolled_window.add(self.files_tv)
self.files_tv_scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
self.edit_tv_scrolled_window = gtk.ScrolledWindow()
self.edit_tv_scrolled_window.add(self.edit_tv)
self.edit_tv_scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
self.mainPaned.add1(self.files_tv_scrolled_window)
self.mainPaned.add2(self.edit_tv_scrolled_window)
self.docBox.add1(self.mainPaned)
self.add(self.rootBox)
self.files_tv.connect('cursor-changed', self.fileSelectionChanged, None)
self.refreshAugeasFileList()
self.currentModule = None
self.set_default_size(800,600)
self.edit_tv.connect("entry-edited", self.__rowEdited, None)
self.edit_tv.connect("entry-toggled", self.__rowToggled, None)
"""
A row was enabled or disabled, update augeas tree, then refresh the view.
"""
def __rowToggled(self, editWidget, path, connectData):
model = editWidget.get_model()
thisIter = model.get_iter_from_string(path)
enabled = model.get_value(thisIter, 0)
aug_root = a.get("/augeas/root")
if not aug_root == '/':
augPath = osp.join('files', osp.relpath(self.currentConfigFilePath, aug_root), self.edit_tv.get_label_path(thisIter))
else:
augPath = osp.join('files', augeas_utils.stripBothSlashes(self.currentConfigFilePath), self.edit_tv.get_label_path(thisIter))
if enabled: #this row was just added, update augeas tree.
indexes = path.split(':')
beforeIndex = int(indexes[len(indexes)-1])-1
if beforeIndex >= 0:
beforePath = ""
for i in indexes[0:len(indexes)-1]:
beforePath = beforePath + str(i) + ":"
beforePath = beforePath + str(beforeIndex)
augBeforePath = self.edit_tv.get_label_path_str(beforePath)
if not aug_root == '/':
augBeforePath = osp.join('files', osp.relpath(self.currentConfigFilePath, aug_root), augBeforePath)
else:
augBeforePath = osp.join('files', augeas_utils.stripBothSlashes(self.currentConfigFilePath), augBeforePath)
self.a.insert(augBeforePath, re.match('^.+/(.+?)(?:\\[[0-9]+\\])?$', augPath).group(1), False)
#print "insert("+augBeforePath+" "+re.match('^.+/(.+?)(?:\\[[0-9]+\\])?$', augPath).group(1)+")"
self.a.set(augPath, '')
else: #this row was deleted, update augeas tree in the refresh
print 'Would remove ' + augPath
self.a.remove(augPath)
self.refreshAugeasEditTree()
"""
This is called every time a selection in the AST edit window is changed.
This function can be used to update documentation, code complete, finalize changes to the AST
"""
def nodeChanged(self, treeview, user_param1):
p = self.edit_tv.getSelectedEntryPath()
p = augeas_utils.removeNumbers(p)
xmlPath = osp.join("/VentooModule/root", p)
docPath = self.currentModule.getDocURLOf(xmlPath)
if docPath == None:
self.docWindow.load_url("about:blank")
else:
self.docWindow.load_url(docPath)
"""
Called when a row value (not label, not enabled/disabled) is changed. update augeas tree.
No need to refresh here.
"""
def __rowEdited(self, editWidget, path, text, connectData):
model = editWidget.get_model()
thisIter = model.get_iter_from_string(path)
enabled = model.get_value(thisIter, 0)
aug_root = a.get("/augeas/root")
#given a iter that was edited, and a current file, build the augeas path to the edited value.
if not aug_root == "/":
augPath = osp.join('files', osp.relpath(self.currentConfigFilePath, aug_root), self.edit_tv.get_label_path(thisIter))
else:
augPath = osp.join('files', augeas_utils.stripBothSlashes(self.currentConfigFilePath), self.edit_tv.get_label_path(thisIter))
enteredValue = model.get_value(thisIter, 2) #get what the user entered.
#print "Setting " + augPath + " = " + enteredValue
self.a.set(augPath, enteredValue)
def diffPressed(self, button, data=None):
#show the diff for the current file.
try:
self.a.save()
except IOError:
pass
#TODO: check augeas/files///error
#to be sure the save worked.
augeas_utils.makeDiffTree(self.a, augeas_utils.getDiffRoot())
diffFiles = [self.currentConfigFilePath, augeas_utils.getDiffLocation(self.a, self.currentConfigFilePath)]
if not osp.isfile(diffFiles[1]):
print "Could not find a diff file...were changes made?"
else:
origFile = open(diffFiles[0])
origList = file.readlines(origFile)
origFile.close()
newFile = open(diffFiles[1])
newList = file.readlines(newFile)
newFile.close()
#now we have origList and newList that is the text for the diff
d = difflib.Differ()
thediff = list(d.compare(origList, newList))
#TODO: append username, to avoid conflicts
outFile = open("/tmp/ventooDiff.html", 'w')
outFile.write("\n")
theDiff = difflib.unified_diff(origList, newList)
text = ""
for l in theDiff:
text += l
highlight(text, DiffLexer(), HtmlFormatter(full=True, linenos=True, cssclass="source"), outFile)
outFile.write("\n")
outFile.close()
self.docWindow.load_url("file:///tmp/ventooDiff.html")
def showErrPressed(self, button, data=None):
d = ErrorDialog.ErrorDialog(augeas_utils.getFileErrorList(self.a))
d.show_all()
d.run()
d.destroy()
def close(widget, event, data=None):
gtk.main_quit()
return False
def refreshAugeasFileList(self):
#reload the file selection list from augeas internals.
self.files_tv.clearFiles()
fileList = augeas_utils.accumulateFiles(a)
for f in fileList:
self.files_tv.addPath(f)
"""
Given an augeas state and a ventoo module update the edit model to show
that augeas state.
The workhorse for this function is __buildEditModel()
"""
def refreshAugeasEditTree(self):
model = self.edit_tv.get_model()
model.clear()
# aRoot = files/etc/foo for the root of the tree to build.
# sketchy file manipulations, TODO: make this more clear.
tmp = augeas_utils.stripTrailingSlash(self.files_tv.getSelectedConfigFilePath())
if sandboxDir != '/':
tmp = osp.relpath(self.files_tv.getSelectedConfigFilePath(), sandboxDir)
aRoot = osp.join('files', augeas_utils.stripBothSlashes(tmp))
# a path into the tree model, coresponds to the aRoot
mRoot = model.get_iter_root()
#path into xml description of the tree
xRoot = '/VentooModule/root'
self.__buildEditModel(model, aRoot, mRoot, xRoot)
"""
this is the workhorse behind refreshAugeasEditTree()
This is the core function for displaying the editing widget tree.
It can be considered the core of the whole program actually.
This code has to be rock solid.
"""
def __buildEditModel(self, model, augeasFileRoot, modelPathIter, xmlRoot):
xElemRoot = self.currentModule.getChildrenOf(osp.join(xmlRoot, '*'))
xChildren = list(self.currentModule.getChildrenOf(xmlRoot))
thisMult = self.currentModule.getMultOf(xmlRoot)
if augeas_utils.isDupLevel(self.a, augeasFileRoot):
#this level is just /1 /2 /3, etc...
#for each match
# created = model.append(modelPathIter, [not addedExtra, str(i), '------'])
# if enabled
# self.__buildEditModel(model, osp.join(augeasFileRoot, str(i)), created, xmlRoot)
matches = self.a.match(osp.join(augeasFileRoot, '*'))
have = 0
maxIndex = 0
#matches / and stores as group 1
indexProg = re.compile('^.+/([0-9]+)/?$')
for match in matches: #add all existing entries
indexResult = indexProg.match(match)
if indexResult != None: #sometimes there are entries on these levels we don't care about.
have += 1
thisIndex = int(indexResult.group(1))
maxIndex = max(maxIndex, thisIndex)
created = model.append(modelPathIter, [True, indexResult.group(1), '-------'])
self.__buildEditModel(model, match, created, xmlRoot)
#add the missing entries
numNeeded = augeas_utils.matchDiff(thisMult, have)
#add the required ones.
for i in range(numNeeded):
created = model.append(modelPathIter, [True, osp.join(augeasFileRoot, str(have+i+1)), '-------'])
self.__buildEditModel(model, match, created, xmlRoot)
have += 1
needOption = not augeas_utils.matchExact(thisMult, have)
if needOption:
created = model.append(modelPathIter, [False, str(have+1), '-------'])
else:
listedNodes = [] #a list of nodes that we already found and know about.
for child in xChildren:
#build get a list of either [child.tag] or [child.tag[1], child.tag[n]]
childMult = self.currentModule.getMultOf(osp.join(xmlRoot, child.tag))
matches = self.a.match(osp.join(augeasFileRoot, child.tag))
matches.extend(self.a.match(osp.join(augeasFileRoot, child.tag)+'[*]'))
listedNodes.extend(matches)
#add leaves if we're missing some required ones (in augeas itself)
have = len(matches)
numNeeded = augeas_utils.matchDiff(childMult, have)
for i in range(have+1, have+numNeeded+1):
p = osp.join(augeasFileRoot, child.tag)
if have+numNeeded > 1:
p = p + '[' + str(i) + ']'
print 'added ' + p + ' to augeas'
self.a.set(p, '')
#update the matches, since we have added stuff to augeas, based on previous matches
matches = self.a.match(osp.join(augeasFileRoot, child.tag))
matches.extend(self.a.match(osp.join(augeasFileRoot, child.tag)+'[*]'))
for match in matches:
userData = self.a.get(match) #add all existing data
if userData == None:
userData = ''
created = model.append(modelPathIter, [True, osp.split(match)[1], userData])
self.__buildEditModel(model, match, created, osp.join(xmlRoot, child.tag))
#maybe we need to add more of child to the tree, and maybe even an option for the user.
have = len(matches)
needed = not augeas_utils.matchExact(childMult, have)
numNeeded = augeas_utils.matchDiff(childMult, have)
if needed:
i = 0
while True:
foo = True
if numNeeded == 0:
foo = False
newLabel = child.tag
if True:
newLabel = newLabel + '['+str(have+i+1)+']'
created = model.append(modelPathIter, [foo, newLabel, ''])
if foo:
self.__buildEditModel(model, 'no_data', created, osp.join(xmlRoot, child.tag))
i += 1
if augeas_utils.matchExact(childMult, have+i):
break
if not foo:
break
#now search for and add nodes that haven't been added yet, and may not be in the VentooModule specifically.
allInAugeas = self.a.match(osp.join(augeasFileRoot, '*'))
for a in allInAugeas:
if not a in listedNodes:
#found that 'a' is not in listedNodes, but is in augeasTree, add it, if it is not supposed to be ignored.
if not osp.split(a)[1].startswith('#'): #always ignore comments
userData = self.a.get(a)
created = model.append(modelPathIter, [True, osp.split(a)[1], userData])
self.__buildEditModel(model, a, created, osp.join(xmlRoot, 'ventoo_dynamic'))
"""
Called when the user picks a new file to view.
"""
def fileSelectionChanged(self, tv, data=None):
#uer picked a new file to edit.
self.currentConfigFilePath = self.files_tv.getSelectedConfigFilePath()
#update the display...and get new module info.
#thse path manipulations are sketchy, should make this code clearer.
tmp = self.currentConfigFilePath
if sandboxDir != '/':
tmp = osp.relpath(self.currentConfigFilePath, sandboxDir)
self.currentModule = VentooModule.VentooModule(augeas_utils.getVentooModuleNameFromSysPath(a, tmp))
self.refreshAugeasEditTree()
if __name__ == '__main__':
if len(sys.argv) > 1:
sandboxDir = sys.argv[1]
if not osp.isdir(sandboxDir):
print sandboxDir + " is not a directory."
sys.exit(0)
print 'Starting augeas...'
#None could be a 'loadpath'
a = augeas.Augeas(sandboxDir, None, augeas.Augeas.SAVE_NEWFILE)
print 'Creating window...'
if sandboxDir == '/':
pass
#Note, it IS possible to create mutiple windows and augeas
#instances to edit multiple "roots" at the same time.
window = MainWindow(a)
window.show_all()
#clear the diff storage place...
shutil.rmtree(augeas_utils.getDiffRoot(), True)
os.makedirs(augeas_utils.getDiffRoot())
gtk.main()