ui.cpp
Go to the documentation of this file.
1 // console user interface
2 // Author: Max Schwarz <max.schwarz@uni-bonn.de>
3 
4 #include "ui.h"
5 #include "husl/husl.h"
6 
7 #include <cstdlib>
8 #include <ros/node_handle.h>
9 
10 #include <fmt/format.h>
11 
12 static unsigned int g_statusLines = 2;
13 static std::string g_windowTitle;
14 
15 void cleanup()
16 {
17  for(unsigned int i = 0; i < g_statusLines+1; ++i)
18  printf("\n");
19 
20  rosmon::Terminal term;
21 
22  // Switch cursor back on
23  term.setCursorVisible();
24 
25  // Switch character echo on
26  term.setEcho(true);
27 
28  // Restore window title (at least try)
29  if(!g_windowTitle.empty())
30  term.clearWindowTitle(g_windowTitle + "[-]");
31 }
32 
33 namespace rosmon
34 {
35 
36 UI::UI(monitor::Monitor* monitor, const FDWatcher::Ptr& fdWatcher)
37  : m_monitor(monitor)
38  , m_fdWatcher(fdWatcher)
39  , m_columns(80)
40  , m_selectedNode(-1)
41 {
42  std::atexit(cleanup);
43  m_monitor->logMessageSignal.connect(boost::bind(&UI::log, this, _1));
44 
45  m_sizeTimer = ros::NodeHandle().createWallTimer(ros::WallDuration(2.0), boost::bind(&UI::checkWindowSize, this));
47 
48  m_terminalCheckTimer = ros::NodeHandle().createWallTimer(ros::WallDuration(0.1), boost::bind(&UI::checkTerminal, this));
50 
52  setupColors();
53 
54  // Switch cursor off
56 
57  // Switch character echo off
58  m_term.setEcho(false);
59 
60  // Configure window title
61  std::string title = m_monitor->config()->windowTitle();
62  if(!title.empty())
63  {
64  m_term.setWindowTitle(title);
65  g_windowTitle = title;
66  }
67 
68  // Setup colors & styles
70  auto white = m_term.color(0xffffff, Terminal::White);
71  auto barFg = m_term.color(0xffffff, Terminal::Black);
72 
77 
84 
89 
90  fdWatcher->registerFD(STDIN_FILENO, boost::bind(&UI::readInput, this));
91 }
92 
94 {
95  m_fdWatcher->removeFD(STDIN_FILENO);
96 }
97 
99 {
100  // Sample colors from the HUSL space
101  int n = m_monitor->nodes().size();
102 
103  for(int i = 0; i < n; ++i)
104  {
105  float hue = i * 360 / n;
106  float sat = 100;
107  float lum = 20;
108 
109  float r, g, b;
110  HUSLtoRGB(&r, &g, &b, hue, sat, lum);
111 
112  r *= 255.0f;
113  g *= 255.0f;
114  b *= 255.0f;
115 
116  unsigned int color =
117  std::min(255, std::max(0, static_cast<int>(r)))
118  | (std::min(255, std::max(0, static_cast<int>(g))) << 8)
119  | (std::min(255, std::max(0, static_cast<int>(b))) << 16);
120 
121  m_nodeColorMap[m_monitor->nodes()[i]->fullName()] = ChannelInfo{&m_term, color};
122  }
123 }
124 
125 // Could be done more elegantly in C++14 with a variadic lambda
126 namespace
127 {
128  class ColumnPrinter
129  {
130  public:
131  constexpr unsigned int column() const
132  { return m_column; }
133 
134  template<typename ... Args>
135  void operator()(Args&& ... args)
136  {
137  std::string str = fmt::format(std::forward<Args>(args)...);
138  m_column += str.size();
139  fputs(str.c_str(), stdout);
140  }
141  private:
142  unsigned int m_column = 0;
143  };
144 }
145 
146 std::string UI::nodeDisplayName(monitor::NodeMonitor& node, std::size_t maxWidth)
147 {
148  std::string fullName;
149  if(node.namespaceString().empty())
150  fullName = node.name();
151  else
152  fullName = node.namespaceString() + "/" + node.name();
153 
154  // Strip initial / to save space
155  if(!fullName.empty() && fullName[0] == '/')
156  fullName = fullName.substr(1);
157 
158  return fullName.substr(0, maxWidth);
159 }
160 
162 {
163  const int NODE_WIDTH = 13;
164 
165  unsigned int lines = 0;
166 
167  // Draw line using UTF-8 box characters
168  {
172  for(int i = 0; i < m_columns; ++i)
173  fmt::print("▂");
174  putchar('\n');
175 
176  lines++;
177  }
178 
179  // Print menu / status line
180  {
183 
184  ColumnPrinter print;
185 
186  auto printKey = [&](const std::string& key, const std::string& label) {
188  print(" {}:", key);
189  m_style_bar.use();
190  print(" {} ", label);
191  };
192 
193  if(m_searchActive)
194  {
196  print("Searching for: {}", m_searchString);
197  m_style_bar.use();
198  }
199  else if(m_selectedNode != -1)
200  {
202  auto& selectedNode = m_monitor->nodes()[m_selectedNode];
203 
204  std::string state;
205  switch(selectedNode->state())
206  {
207  case monitor::NodeMonitor::STATE_RUNNING: state = "is running"; break;
208  case monitor::NodeMonitor::STATE_IDLE: state = "is idle"; break;
209  case monitor::NodeMonitor::STATE_CRASHED: state = "has crashed"; break;
210  case monitor::NodeMonitor::STATE_WAITING: state = "is waiting"; break;
211  default: state = "<UNKNOWN>"; break;
212  }
213 
214  print("Node '{}' {}. Actions: ", selectedNode->fullName(), state);
215  printKey("s", "start");
216  printKey("k", "stop");
217  printKey("d", "debug");
218 
219  if(selectedNode->isMuted())
220  printKey("u", "unmute");
221  else
222  printKey("m", "mute");
223  }
224  else
225  {
226  printKey("A-Z", "Node actions");
227  printKey("F6", "Start all");
228  printKey("F7", "Stop all");
229  printKey("F8", "Toggle WARN+ only");
230  printKey("F9", "Mute all");
231  printKey("F10", "Unmute all");
232  printKey("/", "Node search");
233 
234  if(stderrOnly())
235  {
236  print(" ");
239  print("! WARN+ output only !");
240  m_style_bar.use();
241  }
242 
243  if(anyMuted())
244  {
245  print(" ");
248  print("! Caution: Nodes muted !");
249  m_style_bar.use();
250  }
251  }
252 
253  for(int i = print.column(); i < m_columns; ++i)
254  putchar(' ');
255 
256  putchar('\n');
257 
258  lines++;
259  }
260 
261  int col = 0;
262 
265  if(m_searchActive)
266  {
267  const auto& nodes = m_monitor->nodes();
268  unsigned int i = 0;
269 
270  // We can use the space of the [ ] and key characters
271  constexpr auto SEARCH_NODE_WIDTH = NODE_WIDTH+3;
272 
273  std::size_t nodeWidth = SEARCH_NODE_WIDTH;
274  for(auto& nodeIdx : m_searchNodes)
275  nodeWidth = std::max(nodeWidth, nodeDisplayName(*nodes[nodeIdx]).length());
276 
277  // If it doesn't fit on one line, constrain to SEARCH_NODE_WIDTH
278  if(m_searchNodes.size() * (nodeWidth+1) >= static_cast<std::size_t>(m_columns-1))
279  nodeWidth = SEARCH_NODE_WIDTH;
280 
281  const int BLOCK_WIDTH = nodeWidth;
282  for(auto& nodeIdx : m_searchNodes)
283  {
284  const auto& node = m_monitor->nodes()[nodeIdx];
285 
286  if(i == m_searchSelectedIndex)
288  else
290 
291  std::string label = nodeDisplayName(*node, nodeWidth);
292  fmt::print("{:^{}}", label, nodeWidth);
294 
295  // Primitive wrapping control
296  col += BLOCK_WIDTH;
297 
298  if(col + 1 + BLOCK_WIDTH <= m_columns)
299  {
300  printf(" ");
301  col += 1;
302  }
303  else if(col + 1 + BLOCK_WIDTH > m_columns)
304  {
305  col = 0;
306  lines++;
307  putchar('\n');
309  }
310 
311  ++i;
312  }
313 
314  m_searchDisplayColumns = (m_columns+1) / (BLOCK_WIDTH+1);
315  }
316  else
317  {
318  char key = 'a';
319  int i = 0;
320 
321  for(auto& node : m_monitor->nodes())
322  {
323  if(m_selectedNode == -1)
324  {
325  // Print key with grey background
326  if(node->isMuted())
328  else
330 
331  fmt::print("{:c}", key);
332  }
333  else
334  {
336  fmt::print(" ");
337  }
338 
339  if(m_selectedNode == -1 || m_selectedNode == i)
340  {
341  switch(node->state())
342  {
345  break;
348  break;
351  break;
354  break;
355  }
356  }
357  else
358  {
359  switch(node->state())
360  {
363  break;
366  break;
369  break;
372  break;
373  }
374  }
375 
376  std::string label = nodeDisplayName(*node, NODE_WIDTH);
377  if(i == m_selectedNode)
378  fmt::print("[{:^{}}]", label, NODE_WIDTH);
379  else
380  fmt::print(" {:^{}} ", label, NODE_WIDTH);
382 
383  // Primitive wrapping control
384  const int BLOCK_WIDTH = NODE_WIDTH + 3;
385  col += BLOCK_WIDTH;
386 
387  if(col + 1 + BLOCK_WIDTH <= m_columns)
388  {
389  printf(" ");
390  col += 1;
391  }
392  else if(col + 1 + BLOCK_WIDTH > m_columns)
393  {
394  col = 0;
395  lines++;
396  putchar('\n');
398  }
399 
400  if(key == 'z')
401  key = 'A';
402  else if(key == 'Z')
403  key = '0';
404  else if(key == '9')
405  key = ' ';
406  else if(key != ' ')
407  ++key;
408 
409  ++i;
410  }
411  }
412 
413  // Erase rest of the lines
414  for(unsigned int i = lines; i < g_statusLines; ++i)
415  printf("\n\033[K");
416 
417  g_statusLines = std::max(lines, g_statusLines);
418 }
419 
420 void UI::log(const LogEvent& event)
421 {
422  // Is this node muted? Muted events go into the log, but are not shown in
423  // the UI.
424  if(event.muted)
425  return;
426 
427  // Are we supposed to show stdout?
428  if(event.channel == LogEvent::Channel::Stdout && (!event.showStdout || stderrOnly()))
429  return;
430 
431  std::string clean = event.coloredString();
432 
433  auto it = m_nodeColorMap.find(event.source);
434 
435  // Is this a node message?
436  if(it != m_nodeColorMap.end())
437  {
438  m_term.setLineWrap(false);
439 
440  auto actualLabelWidth = std::max<unsigned int>(m_nodeLabelWidth, event.source.size());
441  auto& parser = (event.channel == LogEvent::Channel::Stderr) ? it->second.stderrParser : it->second.stdoutParser;
442  auto lines = parser.wrap(clean, m_columns - actualLabelWidth - 2);
443 
444  for(unsigned int line = 0; line < lines.size(); ++line)
445  {
446  // Draw label
447  if(m_term.has256Colors())
448  {
449  if(it != m_nodeColorMap.end())
450  {
451  m_term.setBackgroundColor(it->second.labelColor);
453  }
454  }
455  else
456  {
458  }
459 
460  if(line == 0)
461  fmt::print("{:>{}}:", event.source, m_nodeLabelWidth);
462  else
463  {
464  for(unsigned int i = 0; i < actualLabelWidth-1; ++i)
465  putchar(' ');
466  fmt::print("~ ");
467  }
470  putchar(' ');
471 
472  fputs(lines[line].c_str(), stdout);
473  putchar('\n');
474  }
475 
476  m_term.setLineWrap(true);
477  }
478  else
479  {
480  fmt::print("{:>{}}:", event.source, m_nodeLabelWidth);
483  putchar(' ');
484 
485  unsigned int len = clean.length();
486  while(len != 0 && (clean[len-1] == '\n' || clean[len-1] == '\r'))
487  len--;
488 
489  switch(event.type)
490  {
491  case LogEvent::Type::Raw:
493  break;
496  break;
499  break;
502  break;
503  }
504 
505  fwrite(clean.c_str(), 1, len, stdout);
507  putchar('\n');
508  }
509 
512  fflush(stdout);
513 
514  scheduleUpdate();
515 }
516 
518 {
519  if(!m_term.interactive())
520  return;
521 
522  if(!m_refresh_required)
523  return;
524 
525  m_refresh_required = false;
526 
527  // Disable automatic linewrap. This prevents ugliness on terminal resize.
528  m_term.setLineWrap(false);
529 
530  // We currently are at the beginning of the status line.
531  drawStatusLine();
532 
533  // Move back
537 
538  // Enable automatic linewrap again
539  m_term.setLineWrap(true);
540 
541  fflush(stdout);
542 }
543 
545 {
546  int rows, columns;
547  if(m_term.getSize(&columns, &rows))
548  m_columns = columns;
549 
550  std::size_t w = 20;
551  for(const auto& node : m_monitor->nodes())
552  w = std::max(w, node->fullName().size());
553 
554  m_nodeLabelWidth = std::min<unsigned int>(w, m_columns/4);
555 }
556 
558 {
559  int c = m_term.readKey();
560  if(c < 0)
561  return;
562 
563  handleKey(c);
564 }
565 
567 {
568  int c = m_term.readLeftover();
569  if(c < 0)
570  return;
571 
572  handleKey(c);
573 }
574 
575 void UI::handleKey(int c)
576 {
577  // Instead of trying to figure out when exactly we need a redraw, just
578  // redraw on every keystroke.
579  scheduleUpdate();
580 
581  // Are we in search mode?
582  if(m_searchActive)
583  {
584  if(c == '\n')
585  {
588  else
589  m_selectedNode = -1;
590 
591  m_searchActive = false;
592  return;
593  }
594 
595  if(c == '\E')
596  {
597  m_selectedNode = -1;
598  m_searchActive = false;
599  return;
600  }
601 
602  if(c == '\t')
603  {
607 
608  return;
609  }
610 
612  {
615 
616  if(c == Terminal::SK_Right)
617  {
618  if(col < static_cast<int>(m_searchDisplayColumns)-1 && m_searchSelectedIndex < m_searchNodes.size()-1)
620  return;
621  }
622 
623  if(c == Terminal::SK_Left)
624  {
625  if(col > 0)
627  return;
628  }
629 
630  if(c == Terminal::SK_Up)
631  {
632  if(row > 0)
634  return;
635  }
636 
637  if(c == Terminal::SK_Down)
638  {
639  int numRows = (m_searchNodes.size() + m_searchDisplayColumns - 1) / m_searchDisplayColumns;
640  if(row < numRows - 1)
642  return;
643  }
644  }
645 
646  if(c == Terminal::SK_Backspace)
647  {
648  if(!m_searchString.empty())
649  m_searchString.pop_back();
650  }
651  else if(std::isgraph(c))
652  m_searchString.push_back(c);
653 
655 
656  // Recompute matched nodes
657  m_searchNodes.clear();
658  const auto& nodes = m_monitor->nodes();
659  for(unsigned int i = 0; i < nodes.size(); ++i)
660  {
661  const auto& node = nodes[i];
662  auto idx = nodeDisplayName(*node).find(m_searchString);
663  if(idx != std::string::npos)
664  m_searchNodes.push_back(i);
665  }
666 
667  return;
668  }
669 
670  if(m_selectedNode == -1)
671  {
672  int nodeIndex = -1;
673 
674  if(c == Terminal::SK_F6)
675  {
676  startAll();
677  return;
678  }
679 
680  if(c == Terminal::SK_F7)
681  {
682  stopAll();
683  return;
684  }
685 
686  // Check for Mute all keys first
687  if(c == Terminal::SK_F9)
688  {
689  muteAll();
690  return;
691  }
692 
693  if(c == Terminal::SK_F10)
694  {
695  unmuteAll();
696  return;
697  }
698 
699  // Check for Stderr Only Toggle
700  if(c == Terminal::SK_F8)
701  {
703  return;
704  }
705 
706  // Search
707  if(c == '/')
708  {
709  m_searchString = {};
711  m_searchNodes.resize(m_monitor->nodes().size());
712  std::iota(m_searchNodes.begin(), m_searchNodes.end(), 0);
713  m_searchActive = true;
714  return;
715  }
716 
717  if(c >= 'a' && c <= 'z')
718  nodeIndex = c - 'a';
719  else if(c >= 'A' && c <= 'Z')
720  nodeIndex = 26 + c - 'A';
721  else if(c >= '0' && c <= '9')
722  nodeIndex = 26 + 26 + c - '0';
723 
724  if(nodeIndex < 0 || (size_t)nodeIndex >= m_monitor->nodes().size())
725  return;
726 
727  m_selectedNode = nodeIndex;
728  }
729  else
730  {
731  auto& node = m_monitor->nodes()[m_selectedNode];
732 
733  switch(c)
734  {
735  case 's':
736  node->start();
737  break;
738  case 'k':
739  node->stop();
740  break;
741  case 'd':
742  node->launchDebugger();
743  break;
744  case 'm':
745  node->setMuted(true);
746  break;
747  case 'u':
748  node->setMuted(false);
749  break;
750  }
751 
752  m_selectedNode = -1;
753  }
754 }
755 
756 bool UI::anyMuted() const
757 {
758  return std::any_of(m_monitor->nodes().begin(), m_monitor->nodes().end(), [](const monitor::NodeMonitor::Ptr& n){
759  return n->isMuted();
760  });
761 }
762 
764 {
765  for(auto& n : m_monitor->nodes())
766  n->start();
767 }
768 
770 {
771  for(auto& n : m_monitor->nodes())
772  n->stop();
773 }
774 
776 {
777  for(auto& n : m_monitor->nodes())
778  n->setMuted(true);
779 }
780 
782 {
783  for(auto& n : m_monitor->nodes())
784  n->setMuted(false);
785 }
786 
788 {
789  m_refresh_required = true;
790 }
791 
793 {
794  return m_stderr_only;
795 }
796 
798 {
800 }
801 
802 }
~UI()
Definition: ui.cpp:93
boost::signals2::signal< void(LogEvent)> logMessageSignal
Definition: monitor.h:49
std::map< std::string, ChannelInfo > m_nodeColorMap
Definition: ui.h:88
int m_selectedNode
Definition: ui.h:90
void scheduleUpdate()
Definition: ui.cpp:787
bool has256Colors() const
Definition: terminal.cpp:307
bool m_stderr_only
Definition: ui.h:80
bool getSize(int *columns, int *rows)
Get current window size.
Definition: terminal.cpp:480
void setupColors()
Definition: ui.cpp:98
std::string m_searchString
Definition: ui.h:95
Monitors a single node process.
Definition: node_monitor.h:27
void setSimpleBackground(SimpleColor color)
Definition: terminal.cpp:416
void cleanup()
Definition: ui.cpp:15
void moveCursorToStartOfLine()
Move cursor to start of the line.
Definition: terminal.cpp:467
bool m_refresh_required
Definition: ui.h:79
std::string nodeDisplayName(monitor::NodeMonitor &node, std::size_t maxWidth=std::string::npos)
Definition: ui.cpp:146
Color color(SimpleColor code)
Definition: terminal.cpp:616
void checkTerminal()
Definition: ui.cpp:566
FDWatcher::Ptr m_fdWatcher
Definition: ui.h:78
void setBackgroundColor(uint32_t color)
Set 24-bit background color.
Definition: terminal.cpp:341
UI(monitor::Monitor *monitor, const FDWatcher::Ptr &fdWatcher)
Definition: ui.cpp:36
Terminal::Style m_style_nodeIdle
Definition: ui.h:110
Terminal::Style m_style_barLine
Definition: ui.h:102
void startAll()
Definition: ui.cpp:763
ROSCPP_DECL std::string clean(const std::string &name)
Channel channel
Definition: log_event.h:64
Terminal::Style m_style_nodeWaitingFaded
Definition: ui.h:118
unsigned int m_searchSelectedIndex
Definition: ui.h:96
monitor::Monitor * m_monitor
Definition: ui.h:77
ROSCONSOLE_DECL void print(FilterBase *filter, void *logger, Level level, const char *file, int line, const char *function, const char *fmt,...) ROSCONSOLE_PRINTF_ATTRIBUTE(7
void setEcho(bool on)
Definition: terminal.cpp:377
bool stderrOnly()
Definition: ui.cpp:792
void setSimplePair(SimpleColor fg, SimpleColor bg)
Definition: terminal.cpp:425
Terminal::Style m_style_nodeCrashed
Definition: ui.h:112
bool anyMuted() const
Definition: ui.cpp:756
ros::WallTimer m_sizeTimer
Definition: ui.h:85
Idle (e.g. exited with code 0)
Definition: node_monitor.h:36
void update()
Definition: ui.cpp:517
Terminal::Style m_style_nodeWaiting
Definition: ui.h:113
void HUSLtoRGB(float *r, float *g, float *b, float h, float s, float l)
Definition: husl.c:56
Terminal::Style m_style_nodeIdleFaded
Definition: ui.h:115
void setLineWrap(bool on)
Definition: terminal.cpp:472
state
Definition: basic.py:142
void clearToEndOfLine()
Clear characters from cursor to end of line.
Definition: terminal.cpp:451
bool m_searchActive
Definition: ui.h:94
void toggleStderrOnly()
Definition: ui.cpp:797
static std::string g_windowTitle
Definition: ui.cpp:13
Terminal::Style m_style_nodeRunningFaded
Definition: ui.h:116
unsigned int m_nodeLabelWidth
Definition: ui.h:120
Terminal::Style m_style_bar
Definition: ui.h:103
Terminal::Style m_style_nodeKeyMuted
Definition: ui.h:108
Terminal::Style m_style_nodeKey
Definition: ui.h:107
void drawStatusLine()
Definition: ui.cpp:161
std::vector< unsigned int > m_searchNodes
Definition: ui.h:97
unsigned int m_column
Definition: ui.cpp:142
Terminal m_term
Definition: ui.h:82
string b
Definition: busy_node.py:4
void setStandardColors()
Reset fg + bg to standard terminal colors.
Definition: terminal.cpp:434
void checkWindowSize()
Definition: ui.cpp:544
Waiting for automatic restart after crash.
Definition: node_monitor.h:39
Terminal::Color m_color_bar
Definition: ui.h:100
static unsigned int g_statusLines
Definition: ui.cpp:12
void setCursorVisible()
restore cursor
Definition: terminal.cpp:320
void setSimpleForeground(SimpleColor color)
Definition: terminal.cpp:407
void readInput()
Definition: ui.cpp:557
void unmuteAll()
Definition: ui.cpp:781
Encapsulates terminal control.
Definition: terminal.h:22
launch::LaunchConfig::ConstPtr config() const
Definition: monitor.h:46
void moveCursorUp(int numLines)
Move cursor up by numLines.
Definition: terminal.cpp:459
void setWindowTitle(const std::string &title)
Definition: terminal.cpp:493
void log(const LogEvent &event)
Definition: ui.cpp:420
Terminal::Style m_style_nodeRunning
Definition: ui.h:111
void setCursorInvisible()
hide cursor
Definition: terminal.cpp:312
Terminal::Style m_style_barKey
Definition: ui.h:104
Crashed (i.e. exited with code != 0)
Definition: node_monitor.h:38
unsigned int m_searchDisplayColumns
Definition: ui.h:98
void muteAll()
Definition: ui.cpp:775
ros::WallTimer m_terminalCheckTimer
Definition: ui.h:86
std::string name() const
Node name.
Definition: node_monitor.h:167
std::string source
Definition: log_event.h:60
void handleKey(int key)
Definition: ui.cpp:575
void clearWindowTitle(const std::string &backup)
Definition: terminal.cpp:506
Terminal::Style m_style_nodeCrashedFaded
Definition: ui.h:117
Terminal::Style m_style_barHighlight
Definition: ui.h:105
std::shared_ptr< NodeMonitor > Ptr
Definition: node_monitor.h:30
void stopAll()
Definition: ui.cpp:769
bool interactive() const
Definition: terminal.h:217
const std::vector< NodeMonitor::Ptr > & nodes() const
Definition: monitor.h:41
int m_columns
Definition: ui.h:84
std::string namespaceString() const
Node namespace.
Definition: node_monitor.h:171


rosmon_core
Author(s): Max Schwarz
autogenerated on Fri Jun 16 2023 02:15:06