Remove this line
[kdeaccessibility.git] / kttsd / plugins / command / commandproc.cpp
blobbcb2c3869f4214ba5d595979cb19e4d63da4c769
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 <kmouth@schmi-dt.de>
5 Copyright : (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net>
6 -------------------
7 Original author: Gunnar Schmi Dt <kmouth@schmi-dt.de>
8 Current Maintainer: Gary Cramblitt <garycramblitt@comcast.net>
9 ******************************************************************************/
11 /***************************************************************************
12 * *
13 * This program is free software; you can redistribute it and/or modify *
14 * it under the terms of the GNU General Public License as published by *
15 * the Free Software Foundation; either version 2 of the License, or *
16 * (at your option) any later version. *
17 * *
18 ***************************************************************************/
20 // Command Plugin includes.
21 #include "commandproc.h"
22 #include "commandproc.moc"
24 // Qt includes.
25 #include <QtCore/QFile>
26 #include <QtCore/QRegExp>
27 #include <QtCore/QTextCodec>
28 #include <QtCore/QStack>
29 #include <QtCore/QTextStream>
31 // KDE includes.
32 #include <kdebug.h>
33 #include <kconfig.h>
34 #include <kconfiggroup.h>
35 #include <k3process.h>
36 #include <ktemporaryfile.h>
37 #include <kstandarddirs.h>
39 // KTTS includes.
40 #include <pluginproc.h>
42 /** Constructor */
43 CommandProc::CommandProc( QObject* parent, const QStringList& /*args*/) :
44 PlugInProc( parent, "commandproc" )
46 kDebug() << "CommandProc::CommandProc: Running";
47 m_commandProc = 0;
48 m_state = psIdle;
49 m_stdin = true;
50 m_supportsSynth = false;
51 m_waitingStop = false;
54 /** Destructor */
55 CommandProc::~CommandProc()
57 kDebug() << "CommandProc::~CommandProc: Running";
58 if (m_commandProc)
60 if (m_commandProc->isRunning()) m_commandProc->kill();
61 delete m_commandProc;
62 // Don't delete synth file. That is responsibility of caller.
63 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
67 /** Initialize */
68 bool CommandProc::init(KConfig *c, const QString &configGroup){
69 kDebug() << "CommandProc::init: Initializing plug in: Command ";
71 KConfigGroup config(c, configGroup);
72 m_ttsCommand = config.readEntry("Command", "cat -");
73 m_stdin = config.readEntry("StdIn", true);
74 m_language = config.readEntry("LanguageCode", "en");
76 // Support separate synthesis if the TTS command contains %w macro.
77 m_supportsSynth = (m_ttsCommand.contains("%w"));
79 QString codecString = config.readEntry("Codec", "Local");
80 m_codec = codecNameToCodec(codecString);
81 kDebug() << "CommandProc::init: Initialized with command: " << m_ttsCommand << " codec: " << codecString;
82 return true;
85 /**
86 * Say a text. Synthesize and audibilize it.
87 * @param text The text to be spoken.
89 * If the plugin supports asynchronous operation, it should return immediately.
91 void CommandProc::sayText(const QString &text)
93 synth(text, QString(),
94 m_ttsCommand, m_stdin, m_codec, m_language);
97 /**
98 * Synthesize text into an audio file, but do not send to the audio device.
99 * @param text The text to be synthesized.
100 * @param suggestedFilename Full pathname of file to create. The plugin
101 * may ignore this parameter and choose its own
102 * filename. KTTSD will query the generated
103 * filename using getFilename().
105 * If the plugin supports asynchronous operation, it should return immediately.
107 void CommandProc::synthText(const QString& text, const QString& suggestedFilename)
109 synth(text, suggestedFilename,
110 m_ttsCommand, m_stdin, m_codec, m_language);
114 * Say or Synthesize text.
115 * @param inputText The text that shall be spoken
116 * @param suggestedFilename If not Null, synthesize only to this filename, otherwise
117 * synthesize and audibilize the text.
118 * @param userCmd The program that shall be executed for speaking
119 * @param stdIn True if the program shall receive its data via standard input
120 * @param codec The QTextCodec if encoding==UseCodec
121 * @param language The language code (used for the %l macro)
123 void CommandProc::synth(const QString& inputText, const QString& suggestedFilename,
124 const QString& userCmd, bool stdIn, QTextCodec *codec, QString& language)
126 if (m_commandProc)
128 if (m_commandProc->isRunning()) m_commandProc->kill();
129 delete m_commandProc;
130 m_commandProc = 0;
131 m_synthFilename.clear();
132 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
133 m_textFilename.clear();
135 QString command = userCmd;
136 QString text = inputText.trimmed();
137 if (text.isEmpty()) return;
138 // 1. prepare the text:
139 // 1.a) encode the text
140 QByteArray encText;
141 QTextStream ts (encText, QIODevice::WriteOnly);
142 ts.setCodec(codec);
143 ts << text;
144 ts << endl; // Some synths need this, eg. flite.
146 // 1.b) quote the text as one parameter
147 QString escText = K3ShellProcess::quote(text);
149 // 1.c) create a temporary file for the text, if %f macro is used.
150 if (command.contains("%f"))
152 KTemporaryFile tempFile;
153 tempFile.setPrefix("commandplugin-");
154 tempFile.setSuffix(".txt");
155 tempFile.setAutoRemove(false);
156 tempFile.open();
157 QTextStream fs (&tempFile);
158 fs.setCodec(codec);
159 fs << text;
160 fs << endl;
161 m_textFilename = tempFile.fileName();
162 } else m_textFilename.clear();
164 // 2. replace variables with values
165 QStack<bool> stack;
166 bool issinglequote=false;
167 bool isdoublequote=false;
168 int noreplace=0;
169 QRegExp re_noquote("(\"|'|\\\\|`|\\$\\(|\\$\\{|\\(|\\{|\\)|\\}|%%|%t|%f|%l|%w)");
170 QRegExp re_singlequote("('|%%|%t|%f|%l|%w)");
171 QRegExp re_doublequote("(\"|\\\\|`|\\$\\(|\\$\\{|%%|%t|%f|%l|%w)");
173 for ( int i = re_noquote.indexIn(command);
174 i != -1;
175 i = (issinglequote?re_singlequote.indexIn(command,i)
176 :isdoublequote?re_doublequote.indexIn(command,i)
177 :re_noquote.indexIn(command,i))
180 if ((command[i]=='(') || (command[i]=='{')) // (...) or {...}
182 // assert(isdoublequote == false)
183 stack.push(isdoublequote);
184 if (noreplace > 0)
185 // count nested braces when within ${...}
186 noreplace++;
187 i++;
189 else if (command[i]=='$')
191 stack.push(isdoublequote);
192 isdoublequote = false;
193 if ((noreplace > 0) || (command[i+1]=='{'))
194 // count nested braces when within ${...}
195 noreplace++;
196 i+=2;
198 else if ((command[i]==')') || (command[i]=='}'))
199 // $(...) or (...) or ${...} or {...}
201 if (!stack.isEmpty())
202 isdoublequote = stack.pop();
203 else
204 qWarning("Parse error.");
205 if (noreplace > 0)
206 // count nested braces when within ${...}
207 noreplace--;
208 i++;
210 else if (command[i]=='\'')
212 issinglequote=!issinglequote;
213 i++;
215 else if (command[i]=='"')
217 isdoublequote=!isdoublequote;
218 i++;
220 else if (command[i]=='\\')
221 i+=2;
222 else if (command[i]=='`')
224 // Replace all `...` with safer $(...)
225 command.replace (i, 1, "$(");
226 QRegExp re_backticks("(`|\\\\`|\\\\\\\\|\\\\\\$)");
227 for ( int i2=re_backticks.indexIn(command,i+2);
228 i2!=-1;
229 i2=re_backticks.indexIn(command,i2)
232 if (command[i2] == '`')
234 command.replace (i2, 1, ")");
235 i2=command.length(); // leave loop
237 else
238 { // remove backslash and ignore following character
239 command.remove (i2, 1);
240 i2++;
243 // Leave i unchanged! We need to process "$("
245 else if (noreplace == 0) // do not replace macros within ${...}
247 QString match, v;
249 // get match
250 if (issinglequote)
251 match=re_singlequote.cap();
252 else if (isdoublequote)
253 match=re_doublequote.cap();
254 else
255 match=re_noquote.cap();
257 // substitue %variables
258 if (match=="%%")
259 v="%";
260 else if (match=="%t")
261 v=escText;
262 else if (match=="%f")
263 v=m_textFilename;
264 else if (match=="%l")
265 v=language;
266 else if (match=="%w")
267 v = suggestedFilename;
269 // %variable inside of a quote?
270 if (isdoublequote)
271 v='"'+v+'"';
272 else if (issinglequote)
273 v='\''+v+'\'';
275 command.replace (i, match.length(), v);
276 i+=v.length();
278 else
280 if (issinglequote)
281 i+=re_singlequote.matchedLength();
282 else if (isdoublequote)
283 i+=re_doublequote.matchedLength();
284 else
285 i+=re_noquote.matchedLength();
289 // 3. create a new process
290 kDebug() << "CommandProc::synth: running command: " << command;
291 m_commandProc = new K3Process;
292 m_commandProc->setUseShell(true);
293 m_commandProc->setEnvironment("LANG", language + '.' + codec->name());
294 m_commandProc->setEnvironment("LC_CTYPE", language + '.' + codec->name());
295 *m_commandProc << command;
296 connect(m_commandProc, SIGNAL(processExited(K3Process*)),
297 this, SLOT(slotProcessExited(K3Process*)));
298 connect(m_commandProc, SIGNAL(receivedStdout(K3Process*, char*, int)),
299 this, SLOT(slotReceivedStdout(K3Process*, char*, int)));
300 connect(m_commandProc, SIGNAL(receivedStderr(K3Process*, char*, int)),
301 this, SLOT(slotReceivedStderr(K3Process*, char*, int)));
302 connect(m_commandProc, SIGNAL(wroteStdin(K3Process*)),
303 this, SLOT(slotWroteStdin(K3Process* )));
305 // 4. start the process
307 if (suggestedFilename.isNull())
308 m_state = psSaying;
309 else
311 m_synthFilename = suggestedFilename;
312 m_state = psSynthing;
314 if (stdIn) {
315 m_commandProc->start(K3Process::NotifyOnExit, K3Process::All);
316 if (encText.size() > 0)
317 m_commandProc->writeStdin(encText, encText.size());
318 else
319 m_commandProc->closeStdin();
321 else
322 m_commandProc->start(K3Process::NotifyOnExit, K3Process::AllOutput);
326 * Get the generated audio filename from synthText.
327 * @return Name of the audio file the plugin generated.
328 * Null if no such file.
330 * The plugin must not re-use the filename.
332 QString CommandProc::getFilename()
334 kDebug() << "CommandProc::getFilename: returning " << m_synthFilename;
335 return m_synthFilename;
339 * Stop current operation (saying or synthesizing text).
340 * Important: This function may be called from a thread different from the
341 * one that called sayText or synthText.
342 * If the plugin cannot stop an in-progress @ref sayText or
343 * @ref synthText operation, it must not block waiting for it to complete.
344 * Instead, return immediately.
346 * If a plugin returns before the operation has actually been stopped,
347 * the plugin must emit the @ref stopped signal when the operation has
348 * actually stopped.
350 * The plugin should change to the psIdle state after stopping the
351 * operation.
353 void CommandProc::stopText(){
354 kDebug() << "CommandProc::stopText: Running";
355 if (m_commandProc)
357 if (m_commandProc->isRunning())
359 kDebug() << "CommandProc::stopText: killing Command.";
360 m_waitingStop = true;
361 m_commandProc->kill();
362 } else m_state = psIdle;
363 }else m_state = psIdle;
364 kDebug() << "CommandProc::stopText: Command stopped.";
367 void CommandProc::slotProcessExited(K3Process*)
369 kDebug() << "CommandProc:slotProcessExited: Command process has exited.";
370 pluginState prevState = m_state;
371 if (m_waitingStop)
373 m_waitingStop = false;
374 m_state = psIdle;
375 emit stopped();
376 } else {
377 m_state = psFinished;
378 if (prevState == psSaying)
379 emit sayFinished();
380 else
381 if (prevState == psSynthing)
382 emit synthFinished();
386 void CommandProc::slotReceivedStdout(K3Process*, char* buffer, int buflen)
388 QString buf = QString::fromLatin1(buffer, buflen);
389 kDebug() << "CommandProc::slotReceivedStdout: Received output from Command: " << buf;
392 void CommandProc::slotReceivedStderr(K3Process*, char* buffer, int buflen)
394 QString buf = QString::fromLatin1(buffer, buflen);
395 kDebug() << "CommandProc::slotReceivedStderr: Received error from Command: " << buf;
398 void CommandProc::slotWroteStdin(K3Process*)
400 kDebug() << "CommandProc::slotWroteStdin: closing Stdin";
401 m_commandProc->closeStdin();
405 * Return the current state of the plugin.
406 * This function only makes sense in asynchronous mode.
407 * @return The pluginState of the plugin.
409 * @see pluginState
411 pluginState CommandProc::getState() { return m_state; }
414 * Acknowledges a finished state and resets the plugin state to psIdle.
416 * If the plugin is not in state psFinished, nothing happens.
417 * The plugin may use this call to do any post-processing cleanup,
418 * for example, blanking the stored filename (but do not delete the file).
419 * Calling program should call getFilename prior to ackFinished.
421 void CommandProc::ackFinished()
423 if (m_state == psFinished)
425 m_state = psIdle;
426 m_synthFilename.clear();
427 if (!m_textFilename.isNull()) QFile::remove(m_textFilename);
428 m_textFilename.clear();
433 * Returns True if the plugin supports asynchronous processing,
434 * i.e., returns immediately from sayText or synthText.
435 * @return True if this plugin supports asynchronous processing.
437 * If the plugin returns True, it must also implement @ref getState .
438 * It must also emit @ref sayFinished or @ref synthFinished signals when
439 * saying or synthesis is completed.
441 bool CommandProc::supportsAsync() { return true; }
444 * Returns True if the plugin supports synthText method,
445 * i.e., is able to synthesize text to a sound file without
446 * audibilizing the text.
447 * @return True if this plugin supports synthText method.
449 bool CommandProc::supportsSynth() { return m_supportsSynth; }