1
# lyxclient.py -*- coding: iso-8859-1 -*-
2
# Copyright (c) 2005 G�nter Milde
3
# Released under the terms of the GNU General Public License (ver. 2 or later)
5
Classes and functions to implement a generic client talking to LyX via the
8
LyXClient LyX client using the serverpipes
9
More highlevel than the lyxserver.LyXServer class
11
LyXMessage Message from/for the lyxserver
13
LyXError Exception class for errors returned by LyX
15
lyx_remote() open files in a running lyx session (or start a new one)
18
Needs the lyxserver running
19
Set Edit>Preferences>Paths>Server-pipe to "~/.lyx/lyxpipe"
21
Copyright (c) 2005 G�nter Milde
22
Released under the terms of the GNU General Public License (ver. 2 or later)
27
* Server documentation (slightly outdated) is available at
28
LYXDIR/doc/Customization.lyx (but not in the German translation)
30
* A list of all LFUNS is available at
31
http://wiki.lyx.org/pmwiki.php/LyX/LyxFunctions
34
import sys, os, logging
35
from lyxserver import LyXServer, start_lyx
36
from constants import LOG_LEVEL, LYX_OPTIONS, LYXCMD, \
37
LYXSERVER_POLL_TIMEOUT, LYXSERVER_SETUP_RETRIES
39
# set up the logger instance
40
logger = logging.getLogger("lyxclient")
41
# logging.basicConfig() # done in lyxserver.py
42
# set verbosity to show all messages of severity >= LOG_LEVEL
43
logger.setLevel(LOG_LEVEL)
45
class LyXMessage(dict):
46
"""Message from/for the lyxserver pipes
48
Stores the parts of the message in the attributes
49
msg_type -- type of the message
50
client -- name that the client can choose arbitrarily
51
function -- function you want LyX to perform (lyx-function, LFUN)
52
(The same as the commands you'd use in the minibuffer)
53
data -- optional data (function argument, return value,
54
error message, or notifying key)
57
positional argument -- parsed as message string, or
58
keyword arguments -- set matching attributes
60
Converting the object to a message string (suitable for writing to the
61
serverpipe, with newline at the end)
62
str(LyXMessage_instance)
65
str(LyXMessage("LYXCMD:name:fun:arg"))
66
str(LyXMessage(msg_type='LYXCMD', client='name', function='fun',
68
str(LyXMessage("LYXCMD:name:fun:arg", client='another-name'))
70
>>> 'LYXCMD:name:fun:arg\n'
72
str(LyXMessage("NOTIFY:key-sequence"))
73
str(LyXMessage("NOTIFY:key-sequence:unread junk"))
74
str(LyXMessage(type="NOTIFY", data="key-sequence"))
76
>>> 'NOTIFY:key-sequence\n'
78
str(LyXMessage(type="NOTIFY", function="fun", data="key"))
79
would result in the malformed message string.
80
>>> 'NOTIFY:fun:key\n'
82
# Valid message types and recognized fields (as of LyX 1.3.4)
83
# ordered list of all message fields
84
fieldnames = ("msg_type", "client", "function", "data")
85
msg_types = {"LYXCMD": fieldnames,
88
"NOTIFY":("msg_type", "data"),
89
"LYXSRV":("msg_type", "client", "data")
92
def __init__(self, msg_string='', **keywords):
93
"""Parse `msg_string` or set matching attributes.
95
self.__dict__ = self # dictionary elements are also attributes
97
self.parse(msg_string)
99
dict.__init__(self, keywords)
101
def parse(self, msg_string):
102
"""Parse a message string and set attributes
104
self.clear() # delete old attributes
105
# strip trailing newline and split (into <= 4 parts)
106
values = msg_string.rstrip().split(':', 3)
107
# get field names for this message type
108
names = self.msg_types[values[0]] # values[0] holds msg_type
109
fields = dict(zip(names, values))
110
dict.__init__(self, fields)
113
def list_fields(self):
114
"""Return ordered list of field values
117
Empty fields get the value None
118
"""%str(self.fieldnames)
119
return [self.get(name) for name in self.fieldnames]
122
"""Return a string representation suitable to pass to the lyxserver
124
values = filter(bool, self.list_fields()) # filter empty fields
125
return ':'.join(values)+'\n'
128
"""Return an evaluable representation"""
129
args = ["%s=%s"%(name, value) for (name, value) in self.iteritems()]
130
return "LyXMessage(%s)"%", ".join(args)
132
def __eq__(self, other):
133
"""Test for equality.
135
Two Messages are equal, if all fields match. A field value of True
139
LyXMessage("NOTIFY:F12") == LyXMessage("NOTIFY:F12:")
141
LyXMessage("NOTIFY:F12") == LyXMessage("NOTIFY:F11")
143
LyXMessage("NOTIFY:F12") == LyXMessage(msg_type="NOTIFY", data=True)
146
# TODO: test if filtering True values from self.list_fields() is better
147
for name in self.fieldnames:
148
if self.get(name) is True or other.get(name) is True:
150
if self.get(name) != other.get(name):
154
def __ne__(self, other):
155
# needs to be defined separately, see Python documentation
156
return not self.__eq__(other)
158
class LyXError(Exception):
159
"""Exception class for errors returned by LyX"""
160
def __init__(self, strerror):
161
self.strerror = strerror
166
class LyXClient(LyXServer):
167
"""A client that connects to a LyX session via the serverpipes
169
Adds lyx-function calls, message parsing and listening to the LyXServer
171
Calling an instance, sends `function` as a function call to LyX
172
and returns the reply data (an ERROR reply raises a LyXError)
176
bindings = {} # bindings of "notify" keys to actions
179
"""Open the server pipes and register at LyX"""
181
self.write('LYXSRV:%s:hello\n'%self.name)
182
logger.info(self.readmessage(msg_type='LYXSRV'))
185
"""Unregister at LyX and close the server pipes"""
186
# Unregister at LyX (no response will be sent)
188
self.inpipe.write('LYXSRV:%s:bye\n'%self.name)
189
except AttributeError:
191
LyXServer.close(self)
193
def readmessage(self, timeout=None,
194
msg_type=True, client=True, function=True, data=True,
196
"""Read one line of outpipe, return as LyXMessage.
197
(optionally, check for matching fields)
200
timeout -- polling timeout (in ms)
201
not-set or None -> use self.timeout
202
msg_type, client, -- if one of these is not None, the next message
203
function, data with matching field value is picked
204
writeback -- write back last n filtered messages
205
(allows several clients to work in parallel)
207
# template to check the messages against (default: all messages match)
208
pattern = LyXMessage(msg_type=msg_type, client=client,
209
function=function, data=data)
210
junkmessages = [] # leftovers from message picking
211
# logger.debug("readmessage: pattern " + repr(pattern))
212
for msg in self.__iter__(timeout):
213
# logger.debug("readmessage: testing " + repr(msg))
215
# logger.debug("readmessage: match " + str(msg).strip())
217
elif msg: # junk message
218
logger.debug("readmessage: junk message " + str(msg).strip())
219
junkmessages.append(msg)
221
logger.warning("readmessage: no match found")
222
msg = LyXMessage() # empty message
223
else: # empty outpipe
224
logger.warning("readmessage: timed out")
225
msg = LyXMessage() # empty message
226
# write back junk messages
227
if writeback and junkmessages:
228
logger.debug("readmessage: write back last (<=%d) messages"%writeback)
229
# logger.debug(str(junkmessages[-writeback:]))
230
self._writeback(junkmessages[-writeback:])
233
def readmessages(self, timeout=None):
234
"""Read waiting messages. Return list of `LyXMessage` objects.
236
return map(LyXMessage, self.readlines(timeout=None))
238
def _writeback(self, messages):
239
"""Write sequence of messages back to the outpipe"""
240
lines = map(str, messages)
241
# read waiting messages, to preserve the order
242
# in an ideal world, the next 2 lines should be an atomic action
243
lines.extend(self.readlines(timeout=0))
244
self.outpipe.writelines(lines)
246
def write_lfun(self, function, *args):
247
"""send a LFUN to the inpipe"""
248
msg = LyXMessage(msg_type='LYXCMD', client = self.name,
249
function = function, data = ' '.join(map(str, args)))
253
def __iter__(self, timeout=None):
254
"""Return iterator (generator) yielding `LyXMessage(self.readline)`
256
See `LyXServer.readline` for discussion of the `timeout`
260
1. simple call with default timout (self.timeout):
263
2. call with custom timeout:
264
for msg in instance.__iter__(timeout=20):
267
while self.poll(timeout):
268
msg = LyXMessage(self.outpipe.readline())
269
# stop iterating if lyx died (saves waiting for `timeout` ms)
270
if msg.msg_type == "LYXSRV" and msg.data == "bye":
271
logger.info("LyX said bye, closing serverpipes")
272
raise LyXError("LyX closed down")
275
# direct call of instance
276
def __call__(self, function, *args):
277
"""send a LFUN to the inpipe and return the reply data"""
278
self.write_lfun(function, *args)
280
logger.debug("LyXClient.__call__:%r sent, waiting for reply"%function)
281
reply = self.readmessage(timeout=self.timeout, client=self.name,
282
function=function, writeback=5)
284
logger.warning(function + ": no reply")
286
# logger.debug("__call__: reply string: %r"%(reply))
287
if reply.msg_type == 'ERROR':
288
raise LyXError, ':'.join((reply.function, reply.data))
292
def listen(self, timeout=60000):
293
"""wait for a NOTIFY from LyX, run function bound to key
295
self.timeout = timeout
297
logger.debug("listen: new message '%s'"%(str(msg)))
298
if msg.msg_type == 'NOTIFY':
299
logger.info("listen: notice from key '%s'"%msg.data)
300
# call the key binding
302
fun = self.bindings[msg.data]
304
logger.warning("key %s not bound"%msg.data)
305
self("message key %s not bound"%msg.data)
312
except LyXError, exception:
313
logger.warning(exception.strerror)
314
self("message", exception.strerror)
316
logger.debug("listen: junk message '%s'"%(line))
317
# TODO: write back, wait a bit (for others to pick up)
318
# and continue (discarding msg the second time)
320
logger.critical("listen timed out")
323
"""Unregister at LyX and close the server pipes"""
324
logger.info("deleting "+self.name)
328
# lyx-remote: Open files in a lyx session
329
# ---------------------------------------
331
def filter_options(args, valid_options):
332
"""parse a list of arguments and filter the options
334
args -- list of command parameters (e.g. sys.argv[1:])
335
valid options -- dictionary of accepted options with number of
336
option-parameters as value
338
Note: using getopt doesnot work due to 'one-dash long-options' :-(
339
(opts, files) = getopt.getopt(['-help'], LYX_OPTIONS, lyx_long_options)
340
Also the new 'optparse' module doesnot support these.
342
option_parameters = 0
346
if option_parameters:
348
option_parameters -= 1
349
elif arg in valid_options:
351
option_parameters = valid_options[arg]
353
filenames.append(arg)
354
return options, filenames
357
def lyx_remote(cmd=LYXCMD, args=sys.argv[1:]):
358
"""Open all files in `args` in a lyx session.
360
Check for a running LyX session and let it open the files
361
or start LyX in a separate process.
363
cmd -- lyx binary command name (searched on PATH)
364
args -- list of command parameters excluding the command name
365
(default sys.argv[1:])
367
Return LyXServer instance
369
# separate command line options (+ arguments) and filenames
370
options, filenames = filter_options(args, LYX_OPTIONS)
371
# logger.debug("lyx_remote:options:%r,filename:%r"%(options, filenames))
372
# start a new lyx if there are command line options but no files to open
373
if options and not filenames:
374
start_lyx(cmd, options)
376
# move the options (+ option args) to the cmd
377
cmd = " ".join([cmd]+ options)
379
client = LyXClient(lyxcmd=cmd)
381
# Send a LYXCMD for every filename argument
382
for filename in filenames:
383
logger.debug("lyx-remote: opening %s"%filename)
384
client("file-open", os.path.abspath(filename))