#!/usr/bin/env python
# -*- coding: utf-8 -*-
########################################################################################
# #
# Copyright (c) 2009 Jakob Kummerow <[email protected]> #
# #
# This program 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. #
# #
# This program 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 program. If not, see <http://www.gnu.org/licenses/>. #
# #
########################################################################################
"""
Future ideas:
- auto-indenting: automatically increase/decrease indentation level. --> when?
- send further commands to Hugs without restarting --> major change, might need threads
- support GHCI --> would that be useful?
"""
import sys, os, re
from subprocess import Popen, PIPE
from tempfile import NamedTemporaryFile
from PyQt4.QtGui import * #@UnusedWildImport
from PyQt4.QtCore import * #@UnusedWildImport
fileFilter = "Haskell source files (*.hs);;All files (*.*)"
settingsFileName = ".config/hugsgui.conf"
fontfamily = "DejaVu Sans Mono"
class EditBox(QTextEdit):
def __init__(self, parent=None, defaultText=""):
QTextEdit.__init__(self)
self.setTabChangesFocus(False)
self.setAcceptRichText(False)
self.defaultText = defaultText
self.dirty = False
self.loadedFile = False
self.initialState()
self.connect(self, SIGNAL("textChanged()"), self.textChangedHandler)
# we need this to prevent the cursor from jumping back to the system‘s default font
self.connect(self, SIGNAL("currentCharFormatChanged(const QTextCharFormat&)"), self.resetCurrentCharFormat)
self.errorline = []
self.ffont = None
def clearInitially(self):
self.clear()
self.setTextColor(self.palette().color(QPalette.Active, QPalette.Text))
self.neverUsed = False
self.resetFont()
def initialState(self):
self.neverUsed = True
self.setTextColor(self.palette().color(QPalette.Disabled, QPalette.Text))
self.setPlainText(self.defaultText)
def highlightError(self, linenumber):
block = self.document().findBlockByLineNumber(linenumber)
format = block.blockFormat()
format.setBackground(QBrush(QColor(255, 128, 128, 255)))
cursor = self.textCursor()
cursor.setPosition(block.position())
cursor.setBlockFormat(format)
self.errorline += [block]
def resetErrorHighlight(self):
for line in self.errorline:
format = line.blockFormat()
format.clearBackground()
cursor = self.textCursor()
if line.position() < self.document().characterCount():
cursor.setPosition(line.position())
cursor.setBlockFormat(format)
self.errorline = []
def keyPressEvent(self, e):
if self.neverUsed:
self.clearInitially()
if e.key() == Qt.Key_Backtab:
# unindent, either current line only or all selected lines
maincursor = self.textCursor()
if not maincursor.hasSelection():
maincursor.movePosition(QTextCursor.StartOfBlock)
line = str(self.document().findBlockByNumber(maincursor.blockNumber()).text().toUtf8())
whitespace = re.match(r"(\s{0,2})", line).group(1)
for i in range(len(whitespace)): #@UnusedVariable
maincursor.deleteChar()
else:
block = self.document().findBlock(maincursor.selectionStart())
while True:
whitespace = re.match(r"(\s{0,2})", str(block.text().toUtf8())).group(1)
cursor = self.textCursor()
cursor.setPosition(block.position())
for i in range(len(whitespace)): #@UnusedVariable
cursor.deleteChar()
if block.contains(maincursor.selectionEnd()):
break
block = block.next()
e.accept()
elif e.key() == Qt.Key_Tab:
# indent, either current line only or all selected lines
maincursor = self.textCursor()
if not maincursor.hasSelection():
maincursor.insertText(" ")
else:
block = self.document().findBlock(maincursor.selectionStart())
while True:
cursor = self.textCursor()
cursor.setPosition(block.position())
cursor.insertText(" ")
if block.contains(maincursor.selectionEnd()):
break
block = block.next()
e.accept()
elif e.key() == Qt.Key_Return:
# copy whitespace from the beginning of the previous line
cursor = self.textCursor()
block = self.document().findBlockByNumber(cursor.blockNumber())
whitespace = re.match(r"(\s*)", str(block.text().toUtf8())).group(1)
QTextEdit.keyPressEvent(self, e)
cursor = self.textCursor()
format = cursor.blockFormat()
format.clearBackground()
cursor.setBlockFormat(format)
cursor.insertText(whitespace)
else:
QTextEdit.keyPressEvent(self, e)
def focusInEvent(self, e):
if self.neverUsed and e.reason() != Qt.ActiveWindowFocusReason:
self.clearInitially()
QTextEdit.focusInEvent(self, e)
def focusOutEvent(self, e):
if self.loadedFile == False and self.toPlainText() == "":
self.initialState()
QTextEdit.focusOutEvent(self, e)
def mousePressEvent(self, e):
if self.neverUsed:
self.clearInitially()
QTextEdit.mousePressEvent(self, e)
def toPlainText(self):
if self.neverUsed:
return ""
return str(QTextEdit.toPlainText(self).toUtf8())
def textChangedHandler(self):
if self.loadedFile == False and (self.neverUsed or self.toPlainText() == ""):
self.dirty = False
else:
self.dirty = True
def resetFont(self, font=None):
if font != None:
self.ffont = font
newFormat = QTextCharFormat()
if self.ffont == None:
newFormat.setFontFamily(fontfamily)
else:
newFormat.setFont(self.ffont)
cursor = self.textCursor()
cursor.movePosition(QTextCursor.Start)
cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)
cursor.mergeCharFormat(newFormat)
self.setCurrentCharFormat(cursor.charFormat())
def resetCurrentCharFormat(self):
format = self.textCursor().charFormat()
if self.ffont == None:
format.setFontFamily(fontfamily)
else:
format.setFont(self.ffont)
self.setCurrentCharFormat(format)
class HugsGUI(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setWindowTitle(‘Hugs GUI‘)
self.editTextBox = EditBox(self, "Function definition file")
self.commandTextBox = EditBox(self, "Commands for interactive Hugs session")
self.outputTextBox = EditBox(self)
self.outputTextBox.setReadOnly(True)
self.outputTextBox.neverUsed = False
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.editTextBox)
self.splitter.addWidget(self.commandTextBox)
self.splitter.addWidget(self.outputTextBox)
self.setCentralWidget(self.splitter)
toolbar = self.addToolBar("Main")
newAction = QAction("New", self)
newAction.setShortcut("Ctrl+N")
newAction.setToolTip("Create a new file (Ctrl+N)")
self.connect(newAction, SIGNAL("triggered()"), self.newFile)
toolbar.addAction(newAction)
openAction = QAction("Open...", self)
openAction.setShortcut("Ctrl+O")
openAction.setToolTip("Open file (Ctrl+O)")
self.connect(openAction, SIGNAL("triggered()"), self.openFile)
toolbar.addAction(openAction)
saveAction = QAction("Save...", self)
saveAction.setShortcut("Ctrl+S")
saveAction.setToolTip("Save file (Ctrl+S)")
self.connect(saveAction, SIGNAL("triggered()"), self.saveFile)
toolbar.addAction(saveAction)
saveAsAction = QAction("Save as...", self)
self.connect(saveAsAction, SIGNAL("triggered()"), self.saveFileAs)
toolbar.addAction(saveAsAction)
toolbar.addSeparator()
runAction = QAction("Run!", self)
runAction.setShortcut("F9")
runAction.setToolTip("Run (F9)")
self.connect(runAction, SIGNAL("triggered()"), self.runHugs)
toolbar.addAction(runAction)
self.autoSaveAction = QAction("AutoSave before running", self)
self.autoSaveAction.setCheckable(True)
self.autoSaveAction.setToolTip("Whether the current document should automatically be saved before executing it.")
self.interactiveSaveAction = QAction("Save commands for interactive mode", self)
self.interactiveSaveAction.setCheckable(True)
self.interactiveSaveAction.setToolTip("Whether the current command list for the interactive session should be saved on program exit.")
fontAction = QAction("Choose Font...", self)
fontAction.setToolTip("Show a dialog that allows choosing the font to use")
self.connect(fontAction, SIGNAL("triggered()"), self.chooseFont)
optionsbutton = QToolButton()
optionsbutton.setPopupMode(QToolButton.InstantPopup)
optionsbutton.setText("Options")
options = QMenu("Options", optionsbutton)
options.addAction(self.autoSaveAction)
options.addAction(self.interactiveSaveAction)
options.addAction(fontAction)
optionsbutton.setMenu(options)
toolbar.addSeparator()
toolbar.addWidget(optionsbutton)
self.filename = ""
self.ffont = None
self.loadSettings()
def closeEvent(self, event):
if self.editTextBox.dirty:
reply = QMessageBox.question(self, "Question", "Would you like to save your changes before quitting?",
QMessageBox.Save, QMessageBox.Discard, QMessageBox.Cancel)
if reply == QMessageBox.Save:
self.saveFile()
elif reply == QMessageBox.Cancel:
event.ignore()
return
event.accept()
self.saveSettings()
def runHugs(self):
code = self.editTextBox.toPlainText()
if code != "":
if (not self.autoSaveAction.isChecked()) or self.filename == "":
hsfile = NamedTemporaryFile(delete=False)
hsfilepath = hsfile.name
hsfile.file.write(code)
hsfile.file.close()
else:
if self.editTextBox.dirty:
self.saveFile()
hsfilepath = self.filename
else:
hsfilepath = ""
self.outputTextBox.setText("")
echo = self.commandTextBox.toPlainText()
if echo == "":
echo = ":q"
else:
echo = ":set +t\n" + echo
inputfile = NamedTemporaryFile(delete=False)
inputfilepath = inputfile.name
inputfile.file.write(echo)
inputfile.file.close()
if hsfilepath != "":
execstring = str("hugs " + hsfilepath + " < " + inputfilepath)
hugs_out = Popen(execstring, shell=True, stdout=PIPE)
else:
execstring = str("hugs < " + inputfilepath)
hugs_out = Popen(execstring, shell=True, stdout=PIPE)
hugs_out.wait()
show = 0
linenumber = 0
self.editTextBox.resetErrorHighlight()
self.commandTextBox.resetErrorHighlight()
while True:
l = hugs_out.stdout.readline()
if l.rstrip().endswith("> [Leaving Hugs]"):
break
if l.startswith("Type :? for help") or l.startswith("Haskell 98 mode"):
show = 1
elif show == 1:
show = 2
if show == 2 and re.match(r"(.*?)> \1>", l) is not None:
l = l.partition("> ")[2]
show = 3
if show >= 2:
self.outputTextBox.append(l.strip())
if l.startswith("Hugs> ") or l.startswith("Main> "):
if l.startswith("Hugs> ERROR") or l.startswith("Main> ERROR"):
self.commandTextBox.highlightError(linenumber)
linenumber += 1
if l.startswith("ERROR"):
self.highlightError(l)
# clean up
if self.filename == "" and hsfilepath != "":
os.remove(hsfilepath)
os.remove(inputfilepath)
def highlightError(self, line):
linenumber = int(re.match(r"ERROR \".*?\":(\d+) ", line).group(1)) - 1
self.editTextBox.highlightError(linenumber)
def newFile(self):
if self.editTextBox.dirty:
reply = QMessageBox.question(self, "Question", "Would you like to save your changes before creating a new file?",
QMessageBox.Save, QMessageBox.Discard, QMessageBox.Cancel)
if reply == QMessageBox.Save:
self.saveFile()
elif reply == QMessageBox.Cancel:
return
self.editTextBox.clearInitially()
self.editTextBox.dirty = False
self.editTextBox.loadedFile = False
self.filename = ""
def openFile(self):
if self.editTextBox.dirty:
reply = QMessageBox.question(self, "Question", "Would you like to save your changes before loading another file?",
QMessageBox.Save, QMessageBox.Discard, QMessageBox.Cancel)
if reply == QMessageBox.Save:
self.saveFile()
elif reply == QMessageBox.Cancel:
return
filename = QFileDialog.getOpenFileName(self, "Open file", QString(), fileFilter)
if filename != "":
self.filename = filename
self.editTextBox.clearInitially()
with open(filename) as f:
self.editTextBox.setPlainText("".join(f.readlines()))
self.editTextBox.dirty = False
self.editTextBox.loadedFile = True
def saveFileAs(self):
filename = "" + QFileDialog.getSaveFileName(self, "Save file", "untitled.hs", fileFilter).toUtf8()
if filename != "":
self.filename = filename
self.saveFile()
def saveFile(self):
if self.filename == "":
self.saveFileAs()
else:
with open(self.filename, ‘w‘) as f:
f.write(self.editTextBox.toPlainText())
self.editTextBox.dirty = False
def chooseFont(self):
(selectedfont, ok) = QFontDialog.getFont(self.editTextBox.textCursor().charFormat().font(), self)
if ok:
self.ffont = selectedfont
self.setAllFonts()
def setAllFonts(self):
self.editTextBox.resetFont(self.ffont)
self.commandTextBox.resetFont(self.ffont)
self.outputTextBox.resetFont(self.ffont)
def saveSettings(self):
settingsfile = QDir.home().absoluteFilePath(settingsFileName)
settings = QSettings(settingsfile, QSettings.IniFormat)
settings.setValue("View/Size", self.size())
settings.setValue("View/Split", self.splitter.sizes())
settings.setValue("Settings/SaveOnRun", self.autoSaveAction.isChecked())
settings.setValue("Settings/SaveInteractive", self.interactiveSaveAction.isChecked())
if self.interactiveSaveAction.isChecked():
settings.setValue("Interactive/Commands", self.commandTextBox.toPlainText())
if self.ffont != None:
settings.setValue("Settings/Font", self.ffont)
def loadSettings(self):
settingsfile = QDir.home().absoluteFilePath(settingsFileName)
settings = QSettings(settingsfile, QSettings.IniFormat)
self.resize(settings.value("View/Size", QSize(800, 500)).toSize())
splits = settings.value("View/Split", [154, 154, 154]).toList()
for i in range(len(splits)):
(splits[i], ok) = splits[i].toInt() #@UnusedVariable
if splits[i] < 10:
splits[i] = 10
self.splitter.setSizes(splits)
self.autoSaveAction.setChecked(settings.value("Settings/SaveOnRun", True).toBool())
self.interactiveSaveAction.setChecked(settings.value("Settings/SaveInteractive", False).toBool())
if self.interactiveSaveAction.isChecked():
self.commandTextBox.clearInitially()
self.commandTextBox.setPlainText(settings.value("Interactive/Commands", "").toString())
settingsfont = settings.value("Settings/Font", None)
if not settingsfont == QVariant(None):
self.ffont = QFont(settingsfont)
self.setAllFonts()
if __name__ == ‘__main__‘:
app = QApplication(sys.argv)
mainwin = HugsGUI()
mainwin.show()
sys.exit(app.exec_())