Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: code/trunk/src/libraries/core/command/IOConsolePOSIX.cc @ 9550

Last change on this file since 9550 was 9550, checked in by landauf, 11 years ago

merged testing branch back to trunk. unbelievable it took me 13 months to finish this chore…

  • Property svn:eol-style set to native
File size: 16.0 KB
Line 
1/*
2 *   ORXONOX - the hottest 3D action shooter ever to exist
3 *                    > www.orxonox.net <
4 *
5 *
6 *   License notice:
7 *
8 *   This program is free software; you can redistribute it and/or
9 *   modify it under the terms of the GNU General Public License
10 *   as published by the Free Software Foundation; either version 2
11 *   of the License, or (at your option) any later version.
12 *
13 *   This program is distributed in the hope that it will be useful,
14 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
15 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 *   GNU General Public License for more details.
17 *
18 *   You should have received a copy of the GNU General Public License
19 *   along with this program; if not, write to the Free Software
20 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21 *
22 *   Author:
23 *      Oliver Scheuss
24 *      Reto Grieder
25 *   Co-authors:
26 *      ...
27 *
28 */
29
30#include "IOConsole.h"
31
32#include <iomanip>
33#include <iostream>
34#include <termios.h>
35#include <sys/ioctl.h>
36
37#include "util/Clock.h"
38#include "util/Math.h"
39#include "util/output/ConsoleWriter.h"
40#include "util/output/OutputManager.h"
41#include "core/Game.h"
42#include "core/input/InputBuffer.h"
43
44namespace orxonox
45{
46    IOConsole* IOConsole::singletonPtr_s = NULL;
47
48    namespace EscapeMode
49    {
50        enum Value
51        {
52            None,
53            First,
54            Second
55        };
56    }
57
58    IOConsole::IOConsole()
59        : shell_(new Shell("Console", false))
60        , buffer_(shell_->getInputBuffer())
61        , cout_(std::cout.rdbuf())
62        , promptString_("orxonox # ")
63        , bStatusPrinted_(false)
64        , originalTerminalSettings_(0)
65    {
66        this->setTerminalMode();
67        this->shell_->registerListener(this);
68
69        // Manually set the widths of the individual status lines
70        this->statusLineWidths_.push_back(29);
71        this->statusLineMaxWidth_ = 29;
72
73        this->getTerminalSize();
74        this->lastTerminalWidth_ = this->terminalWidth_;
75        this->lastTerminalHeight_ = this->terminalHeight_;
76
77        // Disable standard std::cout logging
78        OutputManager::getInstance().getConsoleWriter()->disable();
79        // Redirect std::cout to an ostringstream
80        // (Other part is in the initialiser list)
81        std::cout.rdbuf(this->origCout_.rdbuf());
82
83        // Make sure we make way for the status lines
84        this->preUpdate(Game::getInstance().getGameClock());
85    }
86
87    IOConsole::~IOConsole()
88    {
89        // Process output written to std::cout in the meantime
90        std::cout.flush();
91        if (!this->origCout_.str().empty())
92            this->shell_->addOutput(this->origCout_.str(), Shell::Cout);
93        // Erase input and status lines
94        this->cout_ << "\033[1G\033[J";
95        // Move cursor to the bottom
96        this->cout_ << "\033[" << this->statusLineWidths_.size() << 'B';
97        // Scroll terminal to compensate for erased lines
98        this->cout_ << "\033[" << this->statusLineWidths_.size() << 'T';
99
100        resetTerminalMode();
101        this->shell_->destroy();
102
103        // Restore this->cout_ redirection
104        std::cout.rdbuf(this->cout_.rdbuf());
105        // Enable standard std::cout logging again
106        OutputManager::getInstance().getConsoleWriter()->enable();
107    }
108
109    void IOConsole::preUpdate(const Clock& time)
110    {
111        unsigned char c;
112        std::string escapeSequence;
113        EscapeMode::Value escapeMode = EscapeMode::None;
114        while (std::cin.good())
115        {
116            c = std::cin.get();
117            if (!std::cin.good())
118                break;
119
120            if (escapeMode == EscapeMode::First && (c == '[' || c=='O') )
121                escapeMode = EscapeMode::Second;
122            // Get Alt+Tab combination when switching applications
123            else if (escapeMode == EscapeMode::First && c == '\t')
124            {
125                this->buffer_->buttonPressed(KeyEvent(KeyCode::Tab, '\t', KeyboardModifier::Alt));
126                escapeMode = EscapeMode::None;
127            }
128            else if (escapeMode == EscapeMode::Second)
129            {
130                escapeSequence += c;
131                escapeMode = EscapeMode::None;
132                if      (escapeSequence == "A")
133                    this->buffer_->buttonPressed(KeyEvent(KeyCode::Up,       0, 0));
134                else if (escapeSequence == "B")
135                    this->buffer_->buttonPressed(KeyEvent(KeyCode::Down,     0, 0));
136                else if (escapeSequence == "C")
137                    this->buffer_->buttonPressed(KeyEvent(KeyCode::Right,    0, 0));
138                else if (escapeSequence == "D")
139                    this->buffer_->buttonPressed(KeyEvent(KeyCode::Left,     0, 0));
140                else if (escapeSequence == "1~" || escapeSequence == "H")
141                    this->buffer_->buttonPressed(KeyEvent(KeyCode::Home,     0, 0));
142                else if (escapeSequence == "2~")
143                    this->buffer_->buttonPressed(KeyEvent(KeyCode::Insert,   0, 0));
144                else if (escapeSequence == "3~")
145                    this->buffer_->buttonPressed(KeyEvent(KeyCode::Delete,   0, 0));
146                else if (escapeSequence == "4~" || escapeSequence == "F")
147                    this->buffer_->buttonPressed(KeyEvent(KeyCode::End,      0, 0));
148                else if (escapeSequence == "5~")
149                    this->buffer_->buttonPressed(KeyEvent(KeyCode::PageUp,   0, 0));
150                else if (escapeSequence == "6~")
151                    this->buffer_->buttonPressed(KeyEvent(KeyCode::PageDown, 0, 0));
152                else
153                    // Waiting for sequence to complete
154                    // If the user presses ESC and then '[' or 'O' while the loop is not
155                    // running (for instance while loading), the whole sequence gets dropped
156                    escapeMode = EscapeMode::Second;
157            }
158            else // not in an escape sequence OR user might have pressed just ESC
159            {
160                if (escapeMode == EscapeMode::First)
161                {
162                    this->buffer_->buttonPressed(KeyEvent(KeyCode::Escape, c, 0));
163                    escapeMode = EscapeMode::None;
164                }
165                if (c == '\033')
166                {
167                    escapeMode = EscapeMode::First;
168                    escapeSequence.clear();
169                }
170                else
171                {
172                    KeyCode::ByEnum code;
173                    switch (c)
174                    {
175                    case '\n'  : case '\r': code = KeyCode::Return; break;
176                    case '\177': case '\b': code = KeyCode::Back;   break;
177                    case '\t'             : code = KeyCode::Tab;    break;
178                    default:
179                        // We don't encode the key code (would be a very large switch)
180                        // because the InputBuffer will only insert the text anyway
181                        // Replacement character is simply KeyCode::A
182                        code = KeyCode::A;
183                    }
184                    this->buffer_->buttonPressed(KeyEvent(code, c, 0));
185                }
186            }
187        }
188        // Reset error flags in std::cin
189        std::cin.clear();
190
191        // If there is still an escape key pending (escape key ONLY), then
192        // it sure isn't an escape sequence anymore
193        if (escapeMode == EscapeMode::First)
194            this->buffer_->buttonPressed(KeyEvent(KeyCode::Escape, '\033', 0));
195
196        // Determine terminal width and height
197        this->lastTerminalWidth_ = this->terminalWidth_;
198        this->lastTerminalHeight_ = this->terminalHeight_;
199        this->getTerminalSize();
200
201        int heightDiff = this->terminalHeight_ - this->lastTerminalHeight_;
202        if (this->bStatusPrinted_ && heightDiff < 0)
203        {
204            // Terminal width has shrunk. The cursor will still be on the input line,
205            // but that line might very well be the last
206            int newLines = std::min((int)this->statusLineWidths_.size(), -heightDiff);
207            // Scroll terminal to create new lines
208            this->cout_ << "\033[" << newLines << 'S';
209        }
210
211        if (!this->bStatusPrinted_ && this->willPrintStatusLines())
212        {
213            // Scroll console to make way for status lines
214            this->cout_ << "\033[" << this->statusLineWidths_.size() << 'S';
215            this->bStatusPrinted_ = true;
216        }
217
218        // We always assume that the cursor is on the input line.
219        // But we cannot always be sure about that, esp. if we scroll the console
220        this->cout_ << "\033[" << this->statusLineWidths_.size() << 'B';
221        this->cout_ << "\033[" << this->statusLineWidths_.size() << 'A';
222
223        // Erase status and input lines
224        this->cout_ << "\033[1G\033[J";
225        this->printInputLine();
226        this->printStatusLines();
227        this->cout_.flush();
228
229        // Process output written to std::cout
230        std::cout.flush();
231        if (!this->origCout_.str().empty())
232        {
233            this->shell_->addOutput(this->origCout_.str(), Shell::Cout);
234            this->origCout_.str("");
235        }
236    }
237
238    void IOConsole::printOutputLine(const std::string& text, Shell::LineType type)
239    {
240        // Colour line
241        switch (type)
242        {
243            case Shell::Message:
244            case Shell::DebugOutput:     this->cout_ << "\033[0m"; break;
245
246            case Shell::UserError:       this->cout_ << "\033[91m"; break;
247            case Shell::UserWarning:     this->cout_ << "\033[93m"; break;
248            case Shell::UserStatus:      this->cout_ << "\033[92m"; break;
249            case Shell::UserInfo:        this->cout_ << "\033[96m"; break;
250
251            case Shell::InternalError:   this->cout_ << "\033[31m"; break;
252            case Shell::InternalWarning: this->cout_ << "\033[33m"; break;
253            case Shell::InternalStatus:  this->cout_ << "\033[32m"; break;
254            case Shell::InternalInfo:    this->cout_ << "\033[36m"; break;
255
256            case Shell::Verbose:         this->cout_ << "\033[94m"; break;
257            case Shell::VerboseMore:     this->cout_ << "\033[34m"; break;
258            case Shell::VerboseUltra:    this->cout_ << "\033[34m"; break;
259
260            case Shell::Command:         this->cout_ << "\033[95m"; break;
261            case Shell::Hint:            this->cout_ << "\033[35m"; break;
262
263            default:                     this->cout_ << "\033[37m"; break;
264        }
265
266        // Print output line
267        this->cout_ << text;
268
269        // Reset colour atributes
270        this->cout_ << "\033[0m";
271    }
272
273    void IOConsole::printInputLine()
274    {
275        // Set cursor to the beginning of the line and erase the line
276        this->cout_ << "\033[1G\033[K";
277        // Indicate a command prompt
278        this->cout_ << this->promptString_;
279        // Save cursor position
280        this->cout_ << "\033[s";
281        // Print command line buffer
282        this->cout_ << this->shell_->getInput();
283        // Restore cursor position and move it to the right
284        this->cout_ << "\033[u";
285        if (this->buffer_->getCursorPosition() > 0)
286            this->cout_ << "\033[" << this->buffer_->getCursorPosition() << 'C';
287    }
288
289    void IOConsole::printStatusLines()
290    {
291        if (this->willPrintStatusLines())
292        {
293            // Save cursor position
294            this->cout_ << "\033[s";
295            // Move cursor down (don't create a new line here because the buffer might flush then!)
296            this->cout_ << "\033[1B\033[1G";
297            this->cout_ << std::fixed << std::setprecision(2) << std::setw(5) << Game::getInstance().getAvgFPS() << " fps, ";
298            this->cout_ <<               std::setprecision(2) << std::setw(5) << Game::getInstance().getAvgTickTime() << " ms tick time";
299            // Restore cursor position
300            this->cout_ << "\033[u";
301            this->bStatusPrinted_ = true;
302        }
303        else
304            this->bStatusPrinted_ = false;
305    }
306
307    void IOConsole::setTerminalMode()
308    {
309        termios new_settings;
310        this->originalTerminalSettings_ = new termios();
311
312        tcgetattr(0, this->originalTerminalSettings_);
313        new_settings = *this->originalTerminalSettings_;
314        new_settings.c_lflag &= ~(ICANON | ECHO);
315        //new_settings.c_lflag |= (ISIG | IEXTEN);
316        new_settings.c_cc[VTIME] = 0;
317        new_settings.c_cc[VMIN]  = 0;
318        tcsetattr(0, TCSANOW, &new_settings);
319        atexit(&IOConsole::resetTerminalMode);
320    }
321
322    /*static*/ void IOConsole::resetTerminalMode()
323    {
324        if (IOConsole::singletonPtr_s && IOConsole::singletonPtr_s->originalTerminalSettings_)
325        {
326            tcsetattr(0, TCSANOW, IOConsole::singletonPtr_s->originalTerminalSettings_);
327            delete IOConsole::singletonPtr_s->originalTerminalSettings_;
328            IOConsole::singletonPtr_s->originalTerminalSettings_ = 0;
329        }
330    }
331
332    void IOConsole::getTerminalSize()
333    {
334        this->terminalWidth_  = 0;
335        this->terminalHeight_ = 0;
336
337#ifdef TIOCGSIZE
338        struct ttysize win;
339        if (!ioctl(STDIN_FILENO, TIOCGSIZE, &win))
340        {
341            this->terminalWidth_  = win.ts_cols;
342            this->terminalHeight_ = win.ts_lines;
343        }
344#elif defined TIOCGWINSZ
345        struct winsize win;
346        if (!ioctl(STDIN_FILENO, TIOCGWINSZ, &win))
347        {
348            this->terminalWidth_  = win.ws_col;
349            this->terminalHeight_ = win.ws_row;
350        }
351#endif
352
353        const char* s;
354        if (!this->terminalWidth_ && (s = getenv("COLUMNS")))
355            this->terminalWidth_  = strtol(s, NULL, 10);
356        if (!this->terminalWidth_)
357            this->terminalWidth_ = 80;
358        if (!this->terminalHeight_ && (s = getenv("LINES")))
359            this->terminalHeight_ = strtol(s, NULL, 10);
360        if (!this->terminalHeight_)
361            this->terminalHeight_ = 24;
362    }
363
364    inline bool IOConsole::willPrintStatusLines()
365    {
366        return !this->statusLineWidths_.empty()
367             && this->terminalWidth_  >= this->statusLineMaxWidth_
368             && this->terminalHeight_ >= this->minOutputLines_ + (int)this->statusLineWidths_.size();
369    }
370
371    // ###############################
372    // ###  ShellListener methods  ###
373    // ###############################
374
375    //! Called if all output-lines have to be reprinted
376    void IOConsole::linesChanged()
377    {
378        // Method only gets called upon start to draw all the lines
379        // or when scrolling. But scrolling is disabled and the output
380        // is already in std::cout when we start the IOConsole
381    }
382
383    //! Called if a command is about to be executed
384    void IOConsole::executed()
385    {
386        this->shell_->addOutput(this->promptString_ + this->shell_->getInput(), Shell::Command);
387    }
388
389    //! Called if the console gets closed
390    void IOConsole::exit()
391    {
392        // Exit is not an option, just do nothing (Shell doesn't really exit too)
393    }
394
395    //! Called if a new output-line was added
396    void IOConsole::lineAdded()
397    {
398        int newLines = this->shell_->getNewestLineIterator()->first.size() / this->terminalWidth_ + 1;
399        // Create new lines by scrolling the screen
400        this->cout_ << "\033[" << newLines << 'S';
401        // Move cursor to the beginning of the new (last) output line
402        this->cout_ << "\033[" << newLines << "A\033[1G";
403        // Erase screen from here
404        this->cout_ << "\033[J";
405        // Print the new output lines
406        for (int i = 0; i < newLines; ++i)
407        {
408            Shell::LineList::const_iterator it = this->shell_->getNewestLineIterator();
409            this->printOutputLine(it->first.substr(i*this->terminalWidth_, this->terminalWidth_), it->second);
410        }
411        // Move cursor down
412        this->cout_ << "\033[1B\033[1G";
413        // Print status and input lines
414        this->printInputLine();
415        this->printStatusLines();
416        this->cout_.flush();
417    }
418
419    //! Called if the text in the input-line has changed
420    void IOConsole::inputChanged()
421    {
422        this->printInputLine();
423        this->cout_.flush();
424    }
425
426    //! Called if the position of the cursor in the input-line has changed
427    void IOConsole::cursorChanged()
428    {
429        this->printInputLine();
430        this->cout_.flush();
431    }
432}
Note: See TracBrowser for help on using the repository browser.