readBoolEntry--
[kdeaccessibility.git] / kttsd / plugins / command / commandproc.cpp
blobd6c91f4842c68001e7f4e9129f5fdab07735ca78
1 /***************************************************** vim:set ts=4 sw=4 sts=4:
2 Main speaking functions for the Command Plug in
3 -------------------
4 Copyright : (C) 2002 by Gunnar Schmi Dt and 2004 by Gary Cramblitt
5 -------------------
6 Original author: Gunnar Schmi Dt <kmouth@schmi-dt.de>
7 Current Maintainer: Gary Cramblitt <garycramblitt@comcast.net>
8 ******************************************************************************/
10 /***************************************************************************
11 * *
12 * This program is free software; you can redistribute it and/or modify *
13 * it under the terms of the GNU General Public License as published by *
14 * the Free Software Foundation; version 2 of the License. *
15 * *
16 ***************************************************************************/
18 // Qt includes.
19 #include <QFile>
20 #include <QString>
21 #include <QStringList>
22 #include <QRegExp>
23 #include <QTextCodec>
24 #include <QStack>
25 #include <QTextStream>
27 // KDE includes.
28 #include <kdebug.h>
29 #include <kconfig.h>
30 #include <kprocess.h>
31 #include <ktempfile.h>
32 #include <kstandarddirs.h>
34 // KTTS includes.
35 #include <pluginproc.h>
37 // Command Plugin includes.
38 #include "commandproc.h"
39 #include "commandproc.moc"
41 /** Constructor */
42 CommandProc::CommandProc( QObject* parent, const char* name, const QStringList& /*args*/) :
43 PlugInProc( parent, name )
45 kdDebug() << "CommandProc::CommandProc: Running" << endl;
46 m_commandProc = 0;
47 m_state = psIdle;
48 m_stdin = true;
49 m_supportsSynth = false;
50 m_waitingStop = false;
53 /** Destructor */
54 CommandProc::~CommandProc()
56 kdDebug() << "CommandProc::~CommandProc: Running" << endl;
57 if (m_commandProc)
59 if (m_commandProc->isRunning()) m_commandProc->kill();
60 delete m_commandProc;
61 // Don't delete synth file. That is responsibility of caller.
62 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
66 /** Initialize */
67 bool CommandProc::init(KConfig *config, const QString &configGroup){
68 kdDebug() << "CommandProc::init: Initializing plug in: Command " << endl;
70 config->setGroup(configGroup);
71 m_ttsCommand = config->readEntry("Command", "cat -");
72 m_stdin = config->readEntry("StdIn", QVariant(true)).toBool();
73 m_language = config->readEntry("LanguageCode", "en");
75 // Support separate synthesis if the TTS command contains %w macro.
76 m_supportsSynth = (m_ttsCommand.contains("%w"));
78 QString codecString = config->readEntry("Codec", "Local");
79 m_codec = codecNameToCodec(codecString);
80 kdDebug() << "CommandProc::init: Initialized with command: " << m_ttsCommand << " codec: " << codecString << endl;
81 return true;
84 /**
85 * Say a text. Synthesize and audibilize it.
86 * @param text The text to be spoken.
88 * If the plugin supports asynchronous operation, it should return immediately.
90 void CommandProc::sayText(const QString &text)
92 synth(text, QString(),
93 m_ttsCommand, m_stdin, m_codec, m_language);
96 /**
97 * Synthesize text into an audio file, but do not send to the audio device.
98 * @param text The text to be synthesized.
99 * @param suggestedFilename Full pathname of file to create. The plugin
100 * may ignore this parameter and choose its own
101 * filename. KTTSD will query the generated
102 * filename using getFilename().
104 * If the plugin supports asynchronous operation, it should return immediately.
106 void CommandProc::synthText(const QString& text, const QString& suggestedFilename)
108 synth(text, suggestedFilename,
109 m_ttsCommand, m_stdin, m_codec, m_language);
113 * Say or Synthesize text.
114 * @param inputText The text that shall be spoken
115 * @param suggestedFilename If not Null, synthesize only to this filename, otherwise
116 * synthesize and audibilize the text.
117 * @param userCmd The program that shall be executed for speaking
118 * @param stdIn True if the program shall recieve its data via standard input
119 * @param codec The QTextCodec if encoding==UseCodec
120 * @param language The language code (used for the %l macro)
122 void CommandProc::synth(const QString& inputText, const QString& suggestedFilename,
123 const QString& userCmd, bool stdIn, QTextCodec *codec, QString& language)
125 if (m_commandProc)
127 if (m_commandProc->isRunning()) m_commandProc->kill();
128 delete m_commandProc;
129 m_commandProc = 0;
130 m_synthFilename.clear();
131 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
132 m_textFilename.clear();
134 QString command = userCmd;
135 QString text = inputText.trimmed();
136 if (text.isEmpty()) return;
137 // 1. prepare the text:
138 // 1.a) encode the text
139 QByteArray encText;
140 QTextStream ts (encText, QIODevice::WriteOnly);
141 ts.setCodec(codec);
142 ts << text;
143 ts << endl; // Some synths need this, eg. flite.
145 // 1.b) quote the text as one parameter
146 QString escText = KShellProcess::quote(text);
148 // 1.c) create a temporary file for the text, if %f macro is used.
149 if (command.contains("%f"))
151 KTempFile tempFile(locateLocal("tmp", "commandplugin-"), ".txt");
152 QTextStream* fs = tempFile.textStream();
153 fs->setCodec(codec);
154 *fs << text;
155 *fs << endl;
156 m_textFilename = tempFile.file()->name();
157 tempFile.close();
158 } else m_textFilename.clear();
160 // 2. replace variables with values
161 QStack<bool> stack;
162 bool issinglequote=false;
163 bool isdoublequote=false;
164 int noreplace=0;
165 QRegExp re_noquote("(\"|'|\\\\|`|\\$\\(|\\$\\{|\\(|\\{|\\)|\\}|%%|%t|%f|%l|%w)");
166 QRegExp re_singlequote("('|%%|%t|%f|%l|%w)");
167 QRegExp re_doublequote("(\"|\\\\|`|\\$\\(|\\$\\{|%%|%t|%f|%l|%w)");
169 for ( int i = re_noquote.search(command);
170 i != -1;
171 i = (issinglequote?re_singlequote.search(command,i)
172 :isdoublequote?re_doublequote.search(command,i)
173 :re_noquote.search(command,i))
176 if ((command[i]=='(') || (command[i]=='{')) // (...) or {...}
178 // assert(isdoublequote == false)
179 stack.push(isdoublequote);
180 if (noreplace > 0)
181 // count nested braces when within ${...}
182 noreplace++;
183 i++;
185 else if (command[i]=='$')
187 stack.push(isdoublequote);
188 isdoublequote = false;
189 if ((noreplace > 0) || (command[i+1]=='{'))
190 // count nested braces when within ${...}
191 noreplace++;
192 i+=2;
194 else if ((command[i]==')') || (command[i]=='}'))
195 // $(...) or (...) or ${...} or {...}
197 if (!stack.isEmpty())
198 isdoublequote = stack.pop();
199 else
200 qWarning("Parse error.");
201 if (noreplace > 0)
202 // count nested braces when within ${...}
203 noreplace--;
204 i++;
206 else if (command[i]=='\'')
208 issinglequote=!issinglequote;
209 i++;
211 else if (command[i]=='"')
213 isdoublequote=!isdoublequote;
214 i++;
216 else if (command[i]=='\\')
217 i+=2;
218 else if (command[i]=='`')
220 // Replace all `...` with safer $(...)
221 command.replace (i, 1, "$(");
222 QRegExp re_backticks("(`|\\\\`|\\\\\\\\|\\\\\\$)");
223 for ( int i2=re_backticks.search(command,i+2);
224 i2!=-1;
225 i2=re_backticks.search(command,i2)
228 if (command[i2] == '`')
230 command.replace (i2, 1, ")");
231 i2=command.length(); // leave loop
233 else
234 { // remove backslash and ignore following character
235 command.remove (i2, 1);
236 i2++;
239 // Leave i unchanged! We need to process "$("
241 else if (noreplace == 0) // do not replace macros within ${...}
243 QString match, v;
245 // get match
246 if (issinglequote)
247 match=re_singlequote.cap();
248 else if (isdoublequote)
249 match=re_doublequote.cap();
250 else
251 match=re_noquote.cap();
253 // substitue %variables
254 if (match=="%%")
255 v="%";
256 else if (match=="%t")
257 v=escText;
258 else if (match=="%f")
259 v=m_textFilename;
260 else if (match=="%l")
261 v=language;
262 else if (match=="%w")
263 v = suggestedFilename;
265 // %variable inside of a quote?
266 if (isdoublequote)
267 v='"'+v+'"';
268 else if (issinglequote)
269 v="'"+v+"'";
271 command.replace (i, match.length(), v);
272 i+=v.length();
274 else
276 if (issinglequote)
277 i+=re_singlequote.matchedLength();
278 else if (isdoublequote)
279 i+=re_doublequote.matchedLength();
280 else
281 i+=re_noquote.matchedLength();
285 // 3. create a new process
286 kdDebug() << "CommandProc::synth: running command: " << command << endl;
287 m_commandProc = new KProcess;
288 m_commandProc->setUseShell(true);
289 m_commandProc->setEnvironment("LANG", language + "." + codec->mimeName());
290 m_commandProc->setEnvironment("LC_CTYPE", language + "." + codec->mimeName());
291 *m_commandProc << command;
292 connect(m_commandProc, SIGNAL(processExited(KProcess*)),
293 this, SLOT(slotProcessExited(KProcess*)));
294 connect(m_commandProc, SIGNAL(receivedStdout(KProcess*, char*, int)),
295 this, SLOT(slotReceivedStdout(KProcess*, char*, int)));
296 connect(m_commandProc, SIGNAL(receivedStderr(KProcess*, char*, int)),
297 this, SLOT(slotReceivedStderr(KProcess*, char*, int)));
298 connect(m_commandProc, SIGNAL(wroteStdin(KProcess*)),
299 this, SLOT(slotWroteStdin(KProcess* )));
301 // 4. start the process
303 if (suggestedFilename.isNull())
304 m_state = psSaying;
305 else
307 m_synthFilename = suggestedFilename;
308 m_state = psSynthing;
310 if (stdIn) {
311 m_commandProc->start(KProcess::NotifyOnExit, KProcess::All);
312 if (encText.size() > 0)
313 m_commandProc->writeStdin(encText, encText.size());
314 else
315 m_commandProc->closeStdin();
317 else
318 m_commandProc->start(KProcess::NotifyOnExit, KProcess::AllOutput);
322 * Get the generated audio filename from synthText.
323 * @return Name of the audio file the plugin generated.
324 * Null if no such file.
326 * The plugin must not re-use the filename.
328 QString CommandProc::getFilename()
330 kdDebug() << "CommandProc::getFilename: returning " << m_synthFilename << endl;
331 return m_synthFilename;
335 * Stop current operation (saying or synthesizing text).
336 * Important: This function may be called from a thread different from the
337 * one that called sayText or synthText.
338 * If the plugin cannot stop an in-progress @ref sayText or
339 * @ref synthText operation, it must not block waiting for it to complete.
340 * Instead, return immediately.
342 * If a plugin returns before the operation has actually been stopped,
343 * the plugin must emit the @ref stopped signal when the operation has
344 * actually stopped.
346 * The plugin should change to the psIdle state after stopping the
347 * operation.
349 void CommandProc::stopText(){
350 kdDebug() << "CommandProc::stopText: Running" << endl;
351 if (m_commandProc)
353 if (m_commandProc->isRunning())
355 kdDebug() << "CommandProc::stopText: killing Command." << endl;
356 m_waitingStop = true;
357 m_commandProc->kill();
358 } else m_state = psIdle;
359 }else m_state = psIdle;
360 kdDebug() << "CommandProc::stopText: Command stopped." << endl;
363 void CommandProc::slotProcessExited(KProcess*)
365 kdDebug() << "CommandProc:slotProcessExited: Command process has exited." << endl;
366 pluginState prevState = m_state;
367 if (m_waitingStop)
369 m_waitingStop = false;
370 m_state = psIdle;
371 emit stopped();
372 } else {
373 m_state = psFinished;
374 if (prevState == psSaying)
375 emit sayFinished();
376 else
377 if (prevState == psSynthing)
378 emit synthFinished();
382 void CommandProc::slotReceivedStdout(KProcess*, char* buffer, int buflen)
384 QString buf = QString::fromLatin1(buffer, buflen);
385 kdDebug() << "CommandProc::slotReceivedStdout: Received output from Command: " << buf << endl;
388 void CommandProc::slotReceivedStderr(KProcess*, char* buffer, int buflen)
390 QString buf = QString::fromLatin1(buffer, buflen);
391 kdDebug() << "CommandProc::slotReceivedStderr: Received error from Command: " << buf << endl;
394 void CommandProc::slotWroteStdin(KProcess*)
396 kdDebug() << "CommandProc::slotWroteStdin: closing Stdin" << endl;
397 m_commandProc->closeStdin();
401 * Return the current state of the plugin.
402 * This function only makes sense in asynchronous mode.
403 * @return The pluginState of the plugin.
405 * @see pluginState
407 pluginState CommandProc::getState() { return m_state; }
410 * Acknowledges a finished state and resets the plugin state to psIdle.
412 * If the plugin is not in state psFinished, nothing happens.
413 * The plugin may use this call to do any post-processing cleanup,
414 * for example, blanking the stored filename (but do not delete the file).
415 * Calling program should call getFilename prior to ackFinished.
417 void CommandProc::ackFinished()
419 if (m_state == psFinished)
421 m_state = psIdle;
422 m_synthFilename.clear();
423 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
424 m_textFilename.clear();
429 * Returns True if the plugin supports asynchronous processing,
430 * i.e., returns immediately from sayText or synthText.
431 * @return True if this plugin supports asynchronous processing.
433 * If the plugin returns True, it must also implement @ref getState .
434 * It must also emit @ref sayFinished or @ref synthFinished signals when
435 * saying or synthesis is completed.
437 bool CommandProc::supportsAsync() { return true; }
440 * Returns True if the plugin supports synthText method,
441 * i.e., is able to synthesize text to a sound file without
442 * audibilizing the text.
443 * @return True if this plugin supports synthText method.
445 bool CommandProc::supportsSynth() { return m_supportsSynth; }