mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
importing from the command line
- a file path provided on the command line now will now trigger the import process - if anki was already running on win/lin, send the import to that process - if no profile was open, wait until profile opened before importing - don't do that for password-protected profiles, as it could be used abusively in a school environment - drop the old pid-based instance check
This commit is contained in:
parent
f7b41a11f6
commit
bc758220a7
7 changed files with 139 additions and 61 deletions
|
@ -1,7 +1,7 @@
|
||||||
# Copyright: Damien Elmes <anki@ichi2.net>
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import os, sys, __builtin__
|
import os, sys, optparse, atexit, __builtin__
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
import locale, gettext
|
import locale, gettext
|
||||||
import anki.lang
|
import anki.lang
|
||||||
|
@ -103,13 +103,78 @@ def setupLang(pm, app, force=None):
|
||||||
|
|
||||||
class AnkiApp(QApplication):
|
class AnkiApp(QApplication):
|
||||||
|
|
||||||
|
# Single instance support on Win32/Linux
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
KEY = "anki"
|
||||||
|
TMOUT = 5000
|
||||||
|
|
||||||
|
def __init__(self, argv):
|
||||||
|
QApplication.__init__(self, argv)
|
||||||
|
self._argv = argv
|
||||||
|
self._shmem = QSharedMemory(self.KEY)
|
||||||
|
self.alreadyRunning = self._shmem.attach()
|
||||||
|
|
||||||
|
def secondInstance(self):
|
||||||
|
if not self.alreadyRunning:
|
||||||
|
# use a 1 byte shared memory instance to signal we exist
|
||||||
|
if not self._shmem.create(1):
|
||||||
|
raise Exception("shared memory not supported")
|
||||||
|
atexit.register(self._shmem.detach)
|
||||||
|
# and a named pipe/unix domain socket for ipc
|
||||||
|
QLocalServer.removeServer(self.KEY)
|
||||||
|
self._srv = QLocalServer(self)
|
||||||
|
self.connect(self._srv, SIGNAL("newConnection()"), self.onRecv)
|
||||||
|
self._srv.listen(self.KEY)
|
||||||
|
# if we were given a file on startup, send import it
|
||||||
|
else:
|
||||||
|
# we accept only one command line argument. if it's missing, send
|
||||||
|
# a blank screen to just raise the existing window
|
||||||
|
opts, args = parseArgs(self._argv)
|
||||||
|
buf = "raise"
|
||||||
|
if args and args[0]:
|
||||||
|
buf = os.path.abspath(args[0])
|
||||||
|
self.sendMsg(buf)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def sendMsg(self, txt):
|
||||||
|
sock = QLocalSocket(self)
|
||||||
|
sock.connectToServer(self.KEY, QIODevice.WriteOnly)
|
||||||
|
if not sock.waitForConnected(self.TMOUT):
|
||||||
|
raise Exception("existing instance not responding")
|
||||||
|
sock.write(txt)
|
||||||
|
if not sock.waitForBytesWritten(self.TMOUT):
|
||||||
|
raise Exception("existing instance not emptying")
|
||||||
|
sock.disconnectFromServer()
|
||||||
|
|
||||||
|
def onRecv(self):
|
||||||
|
sock = self._srv.nextPendingConnection()
|
||||||
|
if not sock.waitForReadyRead(self.TMOUT):
|
||||||
|
sys.stderr.write(sock.errorString())
|
||||||
|
return
|
||||||
|
buf = sock.readAll()
|
||||||
|
self.emit(SIGNAL("appMsg"), unicode(buf))
|
||||||
|
sock.disconnectFromServer()
|
||||||
|
|
||||||
|
# OS X file/url handler
|
||||||
|
##################################################
|
||||||
|
|
||||||
def event(self, evt):
|
def event(self, evt):
|
||||||
from anki.hooks import runHook
|
from anki.hooks import runHook
|
||||||
if evt.type() == QEvent.FileOpen:
|
if evt.type() == QEvent.FileOpen:
|
||||||
runHook("macLoadEvent", unicode(evt.file()))
|
self.emit(SIGNAL("appMsg"), evt.file() or "raise")
|
||||||
return True
|
return True
|
||||||
return QApplication.event(self, evt)
|
return QApplication.event(self, evt)
|
||||||
|
|
||||||
|
def parseArgs(argv):
|
||||||
|
"Returns (opts, args)."
|
||||||
|
parser = optparse.OptionParser()
|
||||||
|
parser.usage = "%prog [OPTIONS] [file to import]"
|
||||||
|
parser.add_option("-b", "--base", help="path to base folder")
|
||||||
|
parser.add_option("-p", "--profile", help="profile name to load")
|
||||||
|
parser.add_option("-l", "--lang", help="interface language (en, de, etc)")
|
||||||
|
return parser.parse_args(argv[1:])
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
global mw
|
global mw
|
||||||
from anki.utils import isWin, isMac
|
from anki.utils import isWin, isMac
|
||||||
|
@ -122,15 +187,12 @@ def run():
|
||||||
# create the app
|
# create the app
|
||||||
app = AnkiApp(sys.argv)
|
app = AnkiApp(sys.argv)
|
||||||
QCoreApplication.setApplicationName("Anki")
|
QCoreApplication.setApplicationName("Anki")
|
||||||
|
if app.secondInstance():
|
||||||
|
# we've signaled the primary instance, so we should close
|
||||||
|
return
|
||||||
|
|
||||||
# parse args
|
# parse args
|
||||||
import optparse
|
opts, args = parseArgs(sys.argv)
|
||||||
parser = optparse.OptionParser()
|
|
||||||
parser.usage = "%prog [OPTIONS]"
|
|
||||||
parser.add_option("-b", "--base", help="path to base folder")
|
|
||||||
parser.add_option("-p", "--profile", help="profile name to load")
|
|
||||||
parser.add_option("-l", "--lang", help="interface language (en, de, etc)")
|
|
||||||
(opts, args) = parser.parse_args(sys.argv[1:])
|
|
||||||
opts.base = unicode(opts.base or "", sys.getfilesystemencoding())
|
opts.base = unicode(opts.base or "", sys.getfilesystemencoding())
|
||||||
opts.profile = unicode(opts.profile or "", sys.getfilesystemencoding())
|
opts.profile = unicode(opts.profile or "", sys.getfilesystemencoding())
|
||||||
|
|
||||||
|
@ -142,12 +204,11 @@ def run():
|
||||||
setupLang(pm, app, opts.lang)
|
setupLang(pm, app, opts.lang)
|
||||||
|
|
||||||
# remaining pm init
|
# remaining pm init
|
||||||
pm.checkPid()
|
|
||||||
pm.ensureProfile()
|
pm.ensureProfile()
|
||||||
|
|
||||||
# load the main window
|
# load the main window
|
||||||
import aqt.main
|
import aqt.main
|
||||||
mw = aqt.main.AnkiQt(app, pm)
|
mw = aqt.main.AnkiQt(app, pm, args)
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -249,6 +249,9 @@ def onImport(mw):
|
||||||
if not file:
|
if not file:
|
||||||
return
|
return
|
||||||
file = unicode(file)
|
file = unicode(file)
|
||||||
|
importFile(mw, file)
|
||||||
|
|
||||||
|
def importFile(mw, file):
|
||||||
ext = os.path.splitext(file)[1]
|
ext = os.path.splitext(file)[1]
|
||||||
importer = None
|
importer = None
|
||||||
done = False
|
done = False
|
||||||
|
|
72
aqt/main.py
72
aqt/main.py
|
@ -21,8 +21,9 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
|
||||||
tooltip, openHelp, openLink
|
tooltip, openHelp, openLink
|
||||||
|
|
||||||
class AnkiQt(QMainWindow):
|
class AnkiQt(QMainWindow):
|
||||||
def __init__(self, app, profileManager):
|
def __init__(self, app, profileManager, args):
|
||||||
QMainWindow.__init__(self)
|
QMainWindow.__init__(self)
|
||||||
|
self.state = "startup"
|
||||||
aqt.mw = self
|
aqt.mw = self
|
||||||
self.app = app
|
self.app = app
|
||||||
self.pm = profileManager
|
self.pm = profileManager
|
||||||
|
@ -43,21 +44,23 @@ class AnkiQt(QMainWindow):
|
||||||
except:
|
except:
|
||||||
showInfo(_("Error during startup:\n%s") % traceback.format_exc())
|
showInfo(_("Error during startup:\n%s") % traceback.format_exc())
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
# were we given a file to import?
|
||||||
|
if args and args[0]:
|
||||||
|
self.onAppMsg(args[0])
|
||||||
# Load profile in a timer so we can let the window finish init and not
|
# Load profile in a timer so we can let the window finish init and not
|
||||||
# close on profile load error.
|
# close on profile load error.
|
||||||
self.progress.timer(10, self.setupProfile, False)
|
self.progress.timer(10, self.setupProfile, False)
|
||||||
|
|
||||||
def setupUI(self):
|
def setupUI(self):
|
||||||
self.col = None
|
self.col = None
|
||||||
self.state = "overview"
|
|
||||||
self.hideSchemaMsg = False
|
self.hideSchemaMsg = False
|
||||||
|
self.setupAppMsg()
|
||||||
self.setupKeys()
|
self.setupKeys()
|
||||||
self.setupThreads()
|
self.setupThreads()
|
||||||
self.setupFonts()
|
self.setupFonts()
|
||||||
self.setupMainWindow()
|
self.setupMainWindow()
|
||||||
self.setupSystemSpecific()
|
self.setupSystemSpecific()
|
||||||
self.setupStyle()
|
self.setupStyle()
|
||||||
self.setupProxy()
|
|
||||||
self.setupMenus()
|
self.setupMenus()
|
||||||
self.setupProgress()
|
self.setupProgress()
|
||||||
self.setupErrorHandler()
|
self.setupErrorHandler()
|
||||||
|
@ -75,6 +78,7 @@ class AnkiQt(QMainWindow):
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def setupProfile(self):
|
def setupProfile(self):
|
||||||
|
self.pendingImport = None
|
||||||
# profile not provided on command line?
|
# profile not provided on command line?
|
||||||
if not self.pm.name:
|
if not self.pm.name:
|
||||||
# if there's a single profile, load it automatically
|
# if there's a single profile, load it automatically
|
||||||
|
@ -91,6 +95,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.loadProfile()
|
self.loadProfile()
|
||||||
|
|
||||||
def showProfileManager(self):
|
def showProfileManager(self):
|
||||||
|
self.state = "profileManager"
|
||||||
d = self.profileDiag = QDialog()
|
d = self.profileDiag = QDialog()
|
||||||
f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
|
f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
|
||||||
f.setupUi(d)
|
f.setupUi(d)
|
||||||
|
@ -210,6 +215,15 @@ Are you sure?""")):
|
||||||
self.raise_()
|
self.raise_()
|
||||||
# maybe sync (will load DB)
|
# maybe sync (will load DB)
|
||||||
self.onSync(auto=True)
|
self.onSync(auto=True)
|
||||||
|
# import pending?
|
||||||
|
if self.pendingImport:
|
||||||
|
if self.pm.profile['key']:
|
||||||
|
showInfo(_("""\
|
||||||
|
To import into a password protected profile, please open the profile before attempting to import."""))
|
||||||
|
else:
|
||||||
|
import aqt.importing
|
||||||
|
aqt.importing.importFile(self, self.pendingImport)
|
||||||
|
self.pendingImport = None
|
||||||
runHook("profileLoaded")
|
runHook("profileLoaded")
|
||||||
|
|
||||||
def unloadProfile(self, browser=True):
|
def unloadProfile(self, browser=True):
|
||||||
|
@ -352,9 +366,13 @@ Are you sure?""")):
|
||||||
"Signal queue needs to be rebuilt when edits are finished or by user."
|
"Signal queue needs to be rebuilt when edits are finished or by user."
|
||||||
self.autosave()
|
self.autosave()
|
||||||
self.resetModal = modal
|
self.resetModal = modal
|
||||||
if self.state in ("overview", "review", "deckBrowser"):
|
if self.interactiveState():
|
||||||
self.moveToState("resetRequired")
|
self.moveToState("resetRequired")
|
||||||
|
|
||||||
|
def interactiveState(self):
|
||||||
|
"True if not in profile manager, syncing, etc."
|
||||||
|
return self.state in ("overview", "review", "deckBrowser")
|
||||||
|
|
||||||
def maybeReset(self):
|
def maybeReset(self):
|
||||||
self.autosave()
|
self.autosave()
|
||||||
if self.state == "resetRequired":
|
if self.state == "resetRequired":
|
||||||
|
@ -995,12 +1013,8 @@ will be lost. Continue?"""))
|
||||||
ws.setFontSize(QWebSettings.DefaultFontSize, self.fontHeight)
|
ws.setFontSize(QWebSettings.DefaultFontSize, self.fontHeight)
|
||||||
|
|
||||||
def setupSystemSpecific(self):
|
def setupSystemSpecific(self):
|
||||||
# use system font for webviews
|
|
||||||
# mac tweaks
|
|
||||||
addHook("macLoadEvent", self.onMacLoad)
|
|
||||||
if isMac:
|
if isMac:
|
||||||
qt_mac_set_menubar_icons(False)
|
qt_mac_set_menubar_icons(False)
|
||||||
#self.setUnifiedTitleAndToolBarOnMac(self.pm.profile['showToolbar'])
|
|
||||||
# mac users expect a minimize option
|
# mac users expect a minimize option
|
||||||
self.minimizeShortcut = QShortcut("Ctrl+M", self)
|
self.minimizeShortcut = QShortcut("Ctrl+M", self)
|
||||||
self.connect(self.minimizeShortcut, SIGNAL("activated()"),
|
self.connect(self.minimizeShortcut, SIGNAL("activated()"),
|
||||||
|
@ -1025,12 +1039,40 @@ will be lost. Continue?"""))
|
||||||
def onMacMinimize(self):
|
def onMacMinimize(self):
|
||||||
self.setWindowState(self.windowState() | Qt.WindowMinimized)
|
self.setWindowState(self.windowState() | Qt.WindowMinimized)
|
||||||
|
|
||||||
def onMacLoad(self, fname):
|
# Single instance support
|
||||||
self.loadDeck(fname)
|
|
||||||
|
|
||||||
# Proxy support
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def setupProxy(self):
|
def setupAppMsg(self):
|
||||||
return
|
self.connect(self.app, SIGNAL("appMsg"), self.onAppMsg)
|
||||||
# need to bundle socksipy and install a default socket handler
|
|
||||||
|
def onAppMsg(self, buf):
|
||||||
|
if self.state == "startup":
|
||||||
|
# try again in a second
|
||||||
|
return self.progress.timer(1000, lambda: self.onAppMsg(buf), False)
|
||||||
|
elif self.state == "profileManager":
|
||||||
|
self.pendingImport = buf
|
||||||
|
return showInfo(_("Deck will be imported when a profile is opened."))
|
||||||
|
if not self.interactiveState() or self.progress.busy():
|
||||||
|
# we can't raise the main window while in profile dialog, syncing, etc
|
||||||
|
if buf != "raise":
|
||||||
|
showInfo(_("""\
|
||||||
|
Please ensure a profile is open and Anki is not busy, then try again."""),
|
||||||
|
parent=None)
|
||||||
|
return
|
||||||
|
# raise window
|
||||||
|
if isWin:
|
||||||
|
# on windows we can raise the window by minimizing and restoring
|
||||||
|
self.showMinimized()
|
||||||
|
self.setWindowState(Qt.WindowActive)
|
||||||
|
self.showNormal()
|
||||||
|
else:
|
||||||
|
# on osx we can raise the window. on unity the icon in the tray will just flash.
|
||||||
|
self.activateWindow()
|
||||||
|
self.raise_()
|
||||||
|
if buf == "raise":
|
||||||
|
return
|
||||||
|
# import
|
||||||
|
if not os.path.exists(buf):
|
||||||
|
return showInfo(_("Provided file does not exist."))
|
||||||
|
import aqt.importing
|
||||||
|
aqt.importing.importFile(self, buf)
|
||||||
|
|
|
@ -82,39 +82,6 @@ Anki can't write to the harddisk. Please see the \
|
||||||
documentation for information on using a flash drive.""")
|
documentation for information on using a flash drive.""")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Pid checking
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
def checkPid(self):
|
|
||||||
p = os.path.join(self.base, "pid")
|
|
||||||
# check if an existing instance is running
|
|
||||||
if os.path.exists(p):
|
|
||||||
pid = int(open(p).read())
|
|
||||||
exists = False
|
|
||||||
if isWin:
|
|
||||||
# no posix on windows, sigh
|
|
||||||
from win32process import EnumProcesses as enum
|
|
||||||
if pid in enum():
|
|
||||||
exists = True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
os.kill(pid, 0)
|
|
||||||
exists = True
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
if exists:
|
|
||||||
QMessageBox.warning(
|
|
||||||
None, "Error", _("""\
|
|
||||||
Anki is already running. Please close the existing copy or restart your \
|
|
||||||
computer."""))
|
|
||||||
raise Exception("Already running")
|
|
||||||
# write out pid to the file
|
|
||||||
open(p, "w").write(str(os.getpid()))
|
|
||||||
# add handler to cleanup on exit
|
|
||||||
def cleanup():
|
|
||||||
os.unlink(p)
|
|
||||||
atexit.register(cleanup)
|
|
||||||
|
|
||||||
# Profile load/save
|
# Profile load/save
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
|
@ -155,3 +155,7 @@ Your pysqlite2 is too old. Anki will appear frozen during long operations."""
|
||||||
def _unsetBusy(self):
|
def _unsetBusy(self):
|
||||||
self._disabled = False
|
self._disabled = False
|
||||||
self.app.restoreOverrideCursor()
|
self.app.restoreOverrideCursor()
|
||||||
|
|
||||||
|
def busy(self):
|
||||||
|
"True if processing."
|
||||||
|
return self._levels
|
||||||
|
|
|
@ -10,6 +10,7 @@ sip.setapi('QUrl', 2)
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
|
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
|
||||||
|
from PyQt4.QtNetwork import QLocalServer, QLocalSocket
|
||||||
from PyQt4 import pyqtconfig
|
from PyQt4 import pyqtconfig
|
||||||
|
|
||||||
def debug():
|
def debug():
|
||||||
|
|
|
@ -25,9 +25,9 @@ def showCritical(text, parent=None, help=""):
|
||||||
"Show a small critical error with an OK button."
|
"Show a small critical error with an OK button."
|
||||||
return showInfo(text, parent, help, "critical")
|
return showInfo(text, parent, help, "critical")
|
||||||
|
|
||||||
def showInfo(text, parent=None, help="", type="info"):
|
def showInfo(text, parent=False, help="", type="info"):
|
||||||
"Show a small info window with an OK button."
|
"Show a small info window with an OK button."
|
||||||
if not parent:
|
if parent is False:
|
||||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||||
if type == "warning":
|
if type == "warning":
|
||||||
icon = QMessageBox.Warning
|
icon = QMessageBox.Warning
|
||||||
|
|
Loading…
Reference in a new issue