|
1 # ui.py - user interface bits for mercurial |
|
2 # |
|
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> |
|
4 # |
|
5 # This software may be used and distributed according to the terms of the |
|
6 # GNU General Public License version 2 or any later version. |
|
7 |
|
8 from i18n import _ |
|
9 import errno, getpass, os, socket, sys, tempfile, traceback |
|
10 import config, util, error |
|
11 |
|
12 class ui(object): |
|
13 def __init__(self, src=None): |
|
14 self._buffers = [] |
|
15 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False |
|
16 self._reportuntrusted = True |
|
17 self._ocfg = config.config() # overlay |
|
18 self._tcfg = config.config() # trusted |
|
19 self._ucfg = config.config() # untrusted |
|
20 self._trustusers = set() |
|
21 self._trustgroups = set() |
|
22 |
|
23 if src: |
|
24 self._tcfg = src._tcfg.copy() |
|
25 self._ucfg = src._ucfg.copy() |
|
26 self._ocfg = src._ocfg.copy() |
|
27 self._trustusers = src._trustusers.copy() |
|
28 self._trustgroups = src._trustgroups.copy() |
|
29 self.environ = src.environ |
|
30 self.fixconfig() |
|
31 else: |
|
32 # shared read-only environment |
|
33 self.environ = os.environ |
|
34 # we always trust global config files |
|
35 for f in util.rcpath(): |
|
36 self.readconfig(f, trust=True) |
|
37 |
|
38 def copy(self): |
|
39 return self.__class__(self) |
|
40 |
|
41 def _is_trusted(self, fp, f): |
|
42 st = util.fstat(fp) |
|
43 if util.isowner(st): |
|
44 return True |
|
45 |
|
46 tusers, tgroups = self._trustusers, self._trustgroups |
|
47 if '*' in tusers or '*' in tgroups: |
|
48 return True |
|
49 |
|
50 user = util.username(st.st_uid) |
|
51 group = util.groupname(st.st_gid) |
|
52 if user in tusers or group in tgroups or user == util.username(): |
|
53 return True |
|
54 |
|
55 if self._reportuntrusted: |
|
56 self.warn(_('Not trusting file %s from untrusted ' |
|
57 'user %s, group %s\n') % (f, user, group)) |
|
58 return False |
|
59 |
|
60 def readconfig(self, filename, root=None, trust=False, |
|
61 sections=None, remap=None): |
|
62 try: |
|
63 fp = open(filename) |
|
64 except IOError: |
|
65 if not sections: # ignore unless we were looking for something |
|
66 return |
|
67 raise |
|
68 |
|
69 cfg = config.config() |
|
70 trusted = sections or trust or self._is_trusted(fp, filename) |
|
71 |
|
72 try: |
|
73 cfg.read(filename, fp, sections=sections, remap=remap) |
|
74 except error.ConfigError, inst: |
|
75 if trusted: |
|
76 raise |
|
77 self.warn(_("Ignored: %s\n") % str(inst)) |
|
78 |
|
79 if self.plain(): |
|
80 for k in ('debug', 'fallbackencoding', 'quiet', 'slash', |
|
81 'logtemplate', 'style', |
|
82 'traceback', 'verbose'): |
|
83 if k in cfg['ui']: |
|
84 del cfg['ui'][k] |
|
85 for k, v in cfg.items('alias'): |
|
86 del cfg['alias'][k] |
|
87 for k, v in cfg.items('defaults'): |
|
88 del cfg['defaults'][k] |
|
89 |
|
90 if trusted: |
|
91 self._tcfg.update(cfg) |
|
92 self._tcfg.update(self._ocfg) |
|
93 self._ucfg.update(cfg) |
|
94 self._ucfg.update(self._ocfg) |
|
95 |
|
96 if root is None: |
|
97 root = os.path.expanduser('~') |
|
98 self.fixconfig(root=root) |
|
99 |
|
100 def fixconfig(self, root=None, section=None): |
|
101 if section in (None, 'paths'): |
|
102 # expand vars and ~ |
|
103 # translate paths relative to root (or home) into absolute paths |
|
104 root = root or os.getcwd() |
|
105 for c in self._tcfg, self._ucfg, self._ocfg: |
|
106 for n, p in c.items('paths'): |
|
107 if not p: |
|
108 continue |
|
109 if '%%' in p: |
|
110 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n") |
|
111 % (n, p, self.configsource('paths', n))) |
|
112 p = p.replace('%%', '%') |
|
113 p = util.expandpath(p) |
|
114 if '://' not in p and not os.path.isabs(p): |
|
115 p = os.path.normpath(os.path.join(root, p)) |
|
116 c.set("paths", n, p) |
|
117 |
|
118 if section in (None, 'ui'): |
|
119 # update ui options |
|
120 self.debugflag = self.configbool('ui', 'debug') |
|
121 self.verbose = self.debugflag or self.configbool('ui', 'verbose') |
|
122 self.quiet = not self.debugflag and self.configbool('ui', 'quiet') |
|
123 if self.verbose and self.quiet: |
|
124 self.quiet = self.verbose = False |
|
125 self._reportuntrusted = self.configbool("ui", "report_untrusted", |
|
126 True) |
|
127 self.tracebackflag = self.configbool('ui', 'traceback', False) |
|
128 |
|
129 if section in (None, 'trusted'): |
|
130 # update trust information |
|
131 self._trustusers.update(self.configlist('trusted', 'users')) |
|
132 self._trustgroups.update(self.configlist('trusted', 'groups')) |
|
133 |
|
134 def setconfig(self, section, name, value, overlay=True): |
|
135 if overlay: |
|
136 self._ocfg.set(section, name, value) |
|
137 self._tcfg.set(section, name, value) |
|
138 self._ucfg.set(section, name, value) |
|
139 self.fixconfig(section=section) |
|
140 |
|
141 def _data(self, untrusted): |
|
142 return untrusted and self._ucfg or self._tcfg |
|
143 |
|
144 def configsource(self, section, name, untrusted=False): |
|
145 return self._data(untrusted).source(section, name) or 'none' |
|
146 |
|
147 def config(self, section, name, default=None, untrusted=False): |
|
148 value = self._data(untrusted).get(section, name, default) |
|
149 if self.debugflag and not untrusted and self._reportuntrusted: |
|
150 uvalue = self._ucfg.get(section, name) |
|
151 if uvalue is not None and uvalue != value: |
|
152 self.debug(_("ignoring untrusted configuration option " |
|
153 "%s.%s = %s\n") % (section, name, uvalue)) |
|
154 return value |
|
155 |
|
156 def configbool(self, section, name, default=False, untrusted=False): |
|
157 v = self.config(section, name, None, untrusted) |
|
158 if v is None: |
|
159 return default |
|
160 if isinstance(v, bool): |
|
161 return v |
|
162 b = util.parsebool(v) |
|
163 if b is None: |
|
164 raise error.ConfigError(_("%s.%s not a boolean ('%s')") |
|
165 % (section, name, v)) |
|
166 return b |
|
167 |
|
168 def configlist(self, section, name, default=None, untrusted=False): |
|
169 """Return a list of comma/space separated strings""" |
|
170 |
|
171 def _parse_plain(parts, s, offset): |
|
172 whitespace = False |
|
173 while offset < len(s) and (s[offset].isspace() or s[offset] == ','): |
|
174 whitespace = True |
|
175 offset += 1 |
|
176 if offset >= len(s): |
|
177 return None, parts, offset |
|
178 if whitespace: |
|
179 parts.append('') |
|
180 if s[offset] == '"' and not parts[-1]: |
|
181 return _parse_quote, parts, offset + 1 |
|
182 elif s[offset] == '"' and parts[-1][-1] == '\\': |
|
183 parts[-1] = parts[-1][:-1] + s[offset] |
|
184 return _parse_plain, parts, offset + 1 |
|
185 parts[-1] += s[offset] |
|
186 return _parse_plain, parts, offset + 1 |
|
187 |
|
188 def _parse_quote(parts, s, offset): |
|
189 if offset < len(s) and s[offset] == '"': # "" |
|
190 parts.append('') |
|
191 offset += 1 |
|
192 while offset < len(s) and (s[offset].isspace() or |
|
193 s[offset] == ','): |
|
194 offset += 1 |
|
195 return _parse_plain, parts, offset |
|
196 |
|
197 while offset < len(s) and s[offset] != '"': |
|
198 if (s[offset] == '\\' and offset + 1 < len(s) |
|
199 and s[offset + 1] == '"'): |
|
200 offset += 1 |
|
201 parts[-1] += '"' |
|
202 else: |
|
203 parts[-1] += s[offset] |
|
204 offset += 1 |
|
205 |
|
206 if offset >= len(s): |
|
207 real_parts = _configlist(parts[-1]) |
|
208 if not real_parts: |
|
209 parts[-1] = '"' |
|
210 else: |
|
211 real_parts[0] = '"' + real_parts[0] |
|
212 parts = parts[:-1] |
|
213 parts.extend(real_parts) |
|
214 return None, parts, offset |
|
215 |
|
216 offset += 1 |
|
217 while offset < len(s) and s[offset] in [' ', ',']: |
|
218 offset += 1 |
|
219 |
|
220 if offset < len(s): |
|
221 if offset + 1 == len(s) and s[offset] == '"': |
|
222 parts[-1] += '"' |
|
223 offset += 1 |
|
224 else: |
|
225 parts.append('') |
|
226 else: |
|
227 return None, parts, offset |
|
228 |
|
229 return _parse_plain, parts, offset |
|
230 |
|
231 def _configlist(s): |
|
232 s = s.rstrip(' ,') |
|
233 if not s: |
|
234 return [] |
|
235 parser, parts, offset = _parse_plain, [''], 0 |
|
236 while parser: |
|
237 parser, parts, offset = parser(parts, s, offset) |
|
238 return parts |
|
239 |
|
240 result = self.config(section, name, untrusted=untrusted) |
|
241 if result is None: |
|
242 result = default or [] |
|
243 if isinstance(result, basestring): |
|
244 result = _configlist(result.lstrip(' ,\n')) |
|
245 if result is None: |
|
246 result = default or [] |
|
247 return result |
|
248 |
|
249 def has_section(self, section, untrusted=False): |
|
250 '''tell whether section exists in config.''' |
|
251 return section in self._data(untrusted) |
|
252 |
|
253 def configitems(self, section, untrusted=False): |
|
254 items = self._data(untrusted).items(section) |
|
255 if self.debugflag and not untrusted and self._reportuntrusted: |
|
256 for k, v in self._ucfg.items(section): |
|
257 if self._tcfg.get(section, k) != v: |
|
258 self.debug(_("ignoring untrusted configuration option " |
|
259 "%s.%s = %s\n") % (section, k, v)) |
|
260 return items |
|
261 |
|
262 def walkconfig(self, untrusted=False): |
|
263 cfg = self._data(untrusted) |
|
264 for section in cfg.sections(): |
|
265 for name, value in self.configitems(section, untrusted): |
|
266 yield section, name, str(value).replace('\n', '\\n') |
|
267 |
|
268 def plain(self): |
|
269 '''is plain mode active? |
|
270 |
|
271 Plain mode means that all configuration variables which affect the |
|
272 behavior and output of Mercurial should be ignored. Additionally, the |
|
273 output should be stable, reproducible and suitable for use in scripts or |
|
274 applications. |
|
275 |
|
276 The only way to trigger plain mode is by setting the `HGPLAIN' |
|
277 environment variable. |
|
278 ''' |
|
279 return 'HGPLAIN' in os.environ |
|
280 |
|
281 def username(self): |
|
282 """Return default username to be used in commits. |
|
283 |
|
284 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL |
|
285 and stop searching if one of these is set. |
|
286 If not found and ui.askusername is True, ask the user, else use |
|
287 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname". |
|
288 """ |
|
289 user = os.environ.get("HGUSER") |
|
290 if user is None: |
|
291 user = self.config("ui", "username") |
|
292 if user is not None: |
|
293 user = os.path.expandvars(user) |
|
294 if user is None: |
|
295 user = os.environ.get("EMAIL") |
|
296 if user is None and self.configbool("ui", "askusername"): |
|
297 user = self.prompt(_("enter a commit username:"), default=None) |
|
298 if user is None and not self.interactive(): |
|
299 try: |
|
300 user = '%s@%s' % (util.getuser(), socket.getfqdn()) |
|
301 self.warn(_("No username found, using '%s' instead\n") % user) |
|
302 except KeyError: |
|
303 pass |
|
304 if not user: |
|
305 raise util.Abort(_('no username supplied (see "hg help config")')) |
|
306 if "\n" in user: |
|
307 raise util.Abort(_("username %s contains a newline\n") % repr(user)) |
|
308 return user |
|
309 |
|
310 def shortuser(self, user): |
|
311 """Return a short representation of a user name or email address.""" |
|
312 if not self.verbose: |
|
313 user = util.shortuser(user) |
|
314 return user |
|
315 |
|
316 def expandpath(self, loc, default=None): |
|
317 """Return repository location relative to cwd or from [paths]""" |
|
318 if "://" in loc or os.path.isdir(os.path.join(loc, '.hg')): |
|
319 return loc |
|
320 |
|
321 path = self.config('paths', loc) |
|
322 if not path and default is not None: |
|
323 path = self.config('paths', default) |
|
324 return path or loc |
|
325 |
|
326 def pushbuffer(self): |
|
327 self._buffers.append([]) |
|
328 |
|
329 def popbuffer(self, labeled=False): |
|
330 '''pop the last buffer and return the buffered output |
|
331 |
|
332 If labeled is True, any labels associated with buffered |
|
333 output will be handled. By default, this has no effect |
|
334 on the output returned, but extensions and GUI tools may |
|
335 handle this argument and returned styled output. If output |
|
336 is being buffered so it can be captured and parsed or |
|
337 processed, labeled should not be set to True. |
|
338 ''' |
|
339 return "".join(self._buffers.pop()) |
|
340 |
|
341 def write(self, *args, **opts): |
|
342 '''write args to output |
|
343 |
|
344 By default, this method simply writes to the buffer or stdout, |
|
345 but extensions or GUI tools may override this method, |
|
346 write_err(), popbuffer(), and label() to style output from |
|
347 various parts of hg. |
|
348 |
|
349 An optional keyword argument, "label", can be passed in. |
|
350 This should be a string containing label names separated by |
|
351 space. Label names take the form of "topic.type". For example, |
|
352 ui.debug() issues a label of "ui.debug". |
|
353 |
|
354 When labeling output for a specific command, a label of |
|
355 "cmdname.type" is recommended. For example, status issues |
|
356 a label of "status.modified" for modified files. |
|
357 ''' |
|
358 if self._buffers: |
|
359 self._buffers[-1].extend([str(a) for a in args]) |
|
360 else: |
|
361 for a in args: |
|
362 sys.stdout.write(str(a)) |
|
363 |
|
364 def write_err(self, *args, **opts): |
|
365 try: |
|
366 if not getattr(sys.stdout, 'closed', False): |
|
367 sys.stdout.flush() |
|
368 for a in args: |
|
369 sys.stderr.write(str(a)) |
|
370 # stderr may be buffered under win32 when redirected to files, |
|
371 # including stdout. |
|
372 if not getattr(sys.stderr, 'closed', False): |
|
373 sys.stderr.flush() |
|
374 except IOError, inst: |
|
375 if inst.errno not in (errno.EPIPE, errno.EIO): |
|
376 raise |
|
377 |
|
378 def flush(self): |
|
379 try: sys.stdout.flush() |
|
380 except: pass |
|
381 try: sys.stderr.flush() |
|
382 except: pass |
|
383 |
|
384 def interactive(self): |
|
385 '''is interactive input allowed? |
|
386 |
|
387 An interactive session is a session where input can be reasonably read |
|
388 from `sys.stdin'. If this function returns false, any attempt to read |
|
389 from stdin should fail with an error, unless a sensible default has been |
|
390 specified. |
|
391 |
|
392 Interactiveness is triggered by the value of the `ui.interactive' |
|
393 configuration variable or - if it is unset - when `sys.stdin' points |
|
394 to a terminal device. |
|
395 |
|
396 This function refers to input only; for output, see `ui.formatted()'. |
|
397 ''' |
|
398 i = self.configbool("ui", "interactive", None) |
|
399 if i is None: |
|
400 try: |
|
401 return sys.stdin.isatty() |
|
402 except AttributeError: |
|
403 # some environments replace stdin without implementing isatty |
|
404 # usually those are non-interactive |
|
405 return False |
|
406 |
|
407 return i |
|
408 |
|
409 def termwidth(self): |
|
410 '''how wide is the terminal in columns? |
|
411 ''' |
|
412 if 'COLUMNS' in os.environ: |
|
413 try: |
|
414 return int(os.environ['COLUMNS']) |
|
415 except ValueError: |
|
416 pass |
|
417 return util.termwidth() |
|
418 |
|
419 def formatted(self): |
|
420 '''should formatted output be used? |
|
421 |
|
422 It is often desirable to format the output to suite the output medium. |
|
423 Examples of this are truncating long lines or colorizing messages. |
|
424 However, this is not often not desirable when piping output into other |
|
425 utilities, e.g. `grep'. |
|
426 |
|
427 Formatted output is triggered by the value of the `ui.formatted' |
|
428 configuration variable or - if it is unset - when `sys.stdout' points |
|
429 to a terminal device. Please note that `ui.formatted' should be |
|
430 considered an implementation detail; it is not intended for use outside |
|
431 Mercurial or its extensions. |
|
432 |
|
433 This function refers to output only; for input, see `ui.interactive()'. |
|
434 This function always returns false when in plain mode, see `ui.plain()'. |
|
435 ''' |
|
436 if self.plain(): |
|
437 return False |
|
438 |
|
439 i = self.configbool("ui", "formatted", None) |
|
440 if i is None: |
|
441 try: |
|
442 return sys.stdout.isatty() |
|
443 except AttributeError: |
|
444 # some environments replace stdout without implementing isatty |
|
445 # usually those are non-interactive |
|
446 return False |
|
447 |
|
448 return i |
|
449 |
|
450 def _readline(self, prompt=''): |
|
451 if sys.stdin.isatty(): |
|
452 try: |
|
453 # magically add command line editing support, where |
|
454 # available |
|
455 import readline |
|
456 # force demandimport to really load the module |
|
457 readline.read_history_file |
|
458 # windows sometimes raises something other than ImportError |
|
459 except Exception: |
|
460 pass |
|
461 line = raw_input(prompt) |
|
462 # When stdin is in binary mode on Windows, it can cause |
|
463 # raw_input() to emit an extra trailing carriage return |
|
464 if os.linesep == '\r\n' and line and line[-1] == '\r': |
|
465 line = line[:-1] |
|
466 return line |
|
467 |
|
468 def prompt(self, msg, default="y"): |
|
469 """Prompt user with msg, read response. |
|
470 If ui is not interactive, the default is returned. |
|
471 """ |
|
472 if not self.interactive(): |
|
473 self.write(msg, ' ', default, "\n") |
|
474 return default |
|
475 try: |
|
476 r = self._readline(msg + ' ') |
|
477 if not r: |
|
478 return default |
|
479 return r |
|
480 except EOFError: |
|
481 raise util.Abort(_('response expected')) |
|
482 |
|
483 def promptchoice(self, msg, choices, default=0): |
|
484 """Prompt user with msg, read response, and ensure it matches |
|
485 one of the provided choices. The index of the choice is returned. |
|
486 choices is a sequence of acceptable responses with the format: |
|
487 ('&None', 'E&xec', 'Sym&link') Responses are case insensitive. |
|
488 If ui is not interactive, the default is returned. |
|
489 """ |
|
490 resps = [s[s.index('&')+1].lower() for s in choices] |
|
491 while True: |
|
492 r = self.prompt(msg, resps[default]) |
|
493 if r.lower() in resps: |
|
494 return resps.index(r.lower()) |
|
495 self.write(_("unrecognized response\n")) |
|
496 |
|
497 def getpass(self, prompt=None, default=None): |
|
498 if not self.interactive(): |
|
499 return default |
|
500 try: |
|
501 return getpass.getpass(prompt or _('password: ')) |
|
502 except EOFError: |
|
503 raise util.Abort(_('response expected')) |
|
504 def status(self, *msg, **opts): |
|
505 '''write status message to output (if ui.quiet is False) |
|
506 |
|
507 This adds an output label of "ui.status". |
|
508 ''' |
|
509 if not self.quiet: |
|
510 opts['label'] = opts.get('label', '') + ' ui.status' |
|
511 self.write(*msg, **opts) |
|
512 def warn(self, *msg, **opts): |
|
513 '''write warning message to output (stderr) |
|
514 |
|
515 This adds an output label of "ui.warning". |
|
516 ''' |
|
517 opts['label'] = opts.get('label', '') + ' ui.warning' |
|
518 self.write_err(*msg, **opts) |
|
519 def note(self, *msg, **opts): |
|
520 '''write note to output (if ui.verbose is True) |
|
521 |
|
522 This adds an output label of "ui.note". |
|
523 ''' |
|
524 if self.verbose: |
|
525 opts['label'] = opts.get('label', '') + ' ui.note' |
|
526 self.write(*msg, **opts) |
|
527 def debug(self, *msg, **opts): |
|
528 '''write debug message to output (if ui.debugflag is True) |
|
529 |
|
530 This adds an output label of "ui.debug". |
|
531 ''' |
|
532 if self.debugflag: |
|
533 opts['label'] = opts.get('label', '') + ' ui.debug' |
|
534 self.write(*msg, **opts) |
|
535 def edit(self, text, user): |
|
536 (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt", |
|
537 text=True) |
|
538 try: |
|
539 f = os.fdopen(fd, "w") |
|
540 f.write(text) |
|
541 f.close() |
|
542 |
|
543 editor = self.geteditor() |
|
544 |
|
545 util.system("%s \"%s\"" % (editor, name), |
|
546 environ={'HGUSER': user}, |
|
547 onerr=util.Abort, errprefix=_("edit failed")) |
|
548 |
|
549 f = open(name) |
|
550 t = f.read() |
|
551 f.close() |
|
552 finally: |
|
553 os.unlink(name) |
|
554 |
|
555 return t |
|
556 |
|
557 def traceback(self, exc=None): |
|
558 '''print exception traceback if traceback printing enabled. |
|
559 only to call in exception handler. returns true if traceback |
|
560 printed.''' |
|
561 if self.tracebackflag: |
|
562 if exc: |
|
563 traceback.print_exception(exc[0], exc[1], exc[2]) |
|
564 else: |
|
565 traceback.print_exc() |
|
566 return self.tracebackflag |
|
567 |
|
568 def geteditor(self): |
|
569 '''return editor to use''' |
|
570 return (os.environ.get("HGEDITOR") or |
|
571 self.config("ui", "editor") or |
|
572 os.environ.get("VISUAL") or |
|
573 os.environ.get("EDITOR", "vi")) |
|
574 |
|
575 def progress(self, topic, pos, item="", unit="", total=None): |
|
576 '''show a progress message |
|
577 |
|
578 With stock hg, this is simply a debug message that is hidden |
|
579 by default, but with extensions or GUI tools it may be |
|
580 visible. 'topic' is the current operation, 'item' is a |
|
581 non-numeric marker of the current position (ie the currently |
|
582 in-process file), 'pos' is the current numeric position (ie |
|
583 revision, bytes, etc.), unit is a corresponding unit label, |
|
584 and total is the highest expected pos. |
|
585 |
|
586 Multiple nested topics may be active at a time. |
|
587 |
|
588 All topics should be marked closed by setting pos to None at |
|
589 termination. |
|
590 ''' |
|
591 |
|
592 if pos == None or not self.debugflag: |
|
593 return |
|
594 |
|
595 if unit: |
|
596 unit = ' ' + unit |
|
597 if item: |
|
598 item = ' ' + item |
|
599 |
|
600 if total: |
|
601 pct = 100.0 * pos / total |
|
602 self.debug('%s:%s %s/%s%s (%4.2f%%)\n' |
|
603 % (topic, item, pos, total, unit, pct)) |
|
604 else: |
|
605 self.debug('%s:%s %s%s\n' % (topic, item, pos, unit)) |
|
606 |
|
607 def log(self, service, message): |
|
608 '''hook for logging facility extensions |
|
609 |
|
610 service should be a readily-identifiable subsystem, which will |
|
611 allow filtering. |
|
612 message should be a newline-terminated string to log. |
|
613 ''' |
|
614 pass |
|
615 |
|
616 def label(self, msg, label): |
|
617 '''style msg based on supplied label |
|
618 |
|
619 Like ui.write(), this just returns msg unchanged, but extensions |
|
620 and GUI tools can override it to allow styling output without |
|
621 writing it. |
|
622 |
|
623 ui.write(s, 'label') is equivalent to |
|
624 ui.write(ui.label(s, 'label')). |
|
625 ''' |
|
626 return msg |