Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

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

Last change on this file since 11458 was 11071, checked in by landauf, 9 years ago

merged branch cpp11_v3 back to trunk

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