//
// Copyright (c) 2024, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 17 Jun 24 Brian Frank Creation
//
using concurrent
**
** Console provides utilities to interact with the terminal console.
** For Java this API is designed to use [jline]`docTools::Setup#jline`
** if installed. In browser JavaScript environments this APIs uses
** the JS debugging window.
**
@Js
abstract const class Console
{
** Get the default console for the virtual machine
static Console cur() { NativeConsole.curNative }
** Construct a console that wraps an output stream.
** The returned console instance is **not** thread safe.
static Console wrap(OutStream out) { OutStreamConsole(out) }
** Number of chars that fit horizontally in console or null if unknown
abstract Int? width()
** Number of lines that fit vertically in console or null if unknown
abstract Int? height()
** Print a message to the console at the debug level
abstract This debug(Obj? msg, Err? err := null)
** Print a message to the console at the informational level
abstract This info(Obj? msg, Err? err := null)
** Print a message to the console at the warning level
abstract This warn(Obj? msg, Err? err := null)
** Print a message to the console at the error level
abstract This err(Obj? msg, Err? err := null)
** Print tabular data to the console:
** - List of list is two dimensional data where first row is header names
** - List of items with an each method will create column per key
** - List of items without each will map to a column of "val"
** - Anything else will be table of one cell table
abstract This table(Obj? obj)
** Clear the console of all text if supported
abstract This clear()
** Enter an indented group level in the console. The JS debug
** window can specify the group to default in a collapsed state (this
** flag is ignored in a standard terminal).
abstract This group(Obj? msg, Bool collapsed := false)
** Exit an indented, collapsable group level
abstract This groupEnd()
** Prompt the user to enter a string from standard input.
** Return null if end of stream has been reached.
abstract Str? prompt(Str msg := "")
** Prompt the user to enter a password string from standard input
** with echo disabled. Return null if end of stream has been reached.
abstract Str? promptPassword(Str msg := "")
}
**************************************************************************
** NativeConsole
**************************************************************************
**
** NativeConsole binds to VM console facilities
**
@NoDoc @Js
native const class NativeConsole : Console
{
static NativeConsole curNative()
override Int? width()
override Int? height()
override This debug(Obj? msg, Err? err := null)
override This info(Obj? msg, Err? err := null)
override This warn(Obj? msg, Err? err := null)
override This err(Obj? msg, Err? err := null)
override This table(Obj? obj)
override This clear()
override This group(Obj? msg, Bool collapsed := false)
override This groupEnd()
override Str? prompt(Str msg := "")
override Str? promptPassword(Str msg := "")
}
**************************************************************************
** OutStreamConsole
**************************************************************************
**
** OutStreamConsole writes to an output stream (not thread safe)
**
@NoDoc @Js
const class OutStreamConsole : Console
{
new make(OutStream out) { this.outRef = Unsafe(out) }
override Int? width() { null }
override Int? height() { null }
override This debug(Obj? msg, Err? err := null) { log("DEBUG", msg, err) }
override This info(Obj? msg, Err? err := null) { log(null, msg, err) }
override This warn(Obj? msg, Err? err := null) { log("WARN", msg, err) }
override This err(Obj? msg, Err? err := null) { log("ERR", msg, err) }
override This table(Obj? obj) { ConsoleTable(obj).dump(this); return this }
override This clear() { this }
override This group(Obj? msg, Bool collapsed := false) { info(msg); indent.increment; return this }
override This groupEnd() { indent.decrement; return this }
override Str? prompt(Str msg := "") { throw UnsupportedErr() }
override Str? promptPassword(Str msg := "") { throw UnsupportedErr() }
virtual This log(Str? level, Str msg, Err? err := null)
{
out.print(Str.spaces(indent.val * 2))
if (level != null) out.print(level).print(": ")
out.printLine(msg)
if (err != null) err.traceToStr.splitLines.each |line| { out.print(level).printLine(line) }
return this
}
OutStream out() { outRef.val }
const Unsafe outRef
const AtomicInt indent := AtomicInt()
}
**************************************************************************
** ConsoleTable
**************************************************************************
**
** ConsoleTable is helper class to coerce objects to tables
**
@NoDoc @Js
class ConsoleTable
{
new make(Obj? x)
{
list := x as List
// list of lists
if (list != null && list.first is List)
{
headers = list[0]
if (list.size > 1) rows = list[1..-1]
return
}
// list of something
if (list != null)
{
// turn each item in list to a Str:Str map
maps := Str:Str[,]
list.each |item| { maps.add(map(item)) }
// create list of columns union
cols := Str:Str[:] { ordered = true }
maps.each |map|
{
map.each |v, k| { cols[k] = k }
}
headers = cols.vals
// now turn each row Str:Str into a Str[] of cells
maps.each |map|
{
row := Str[,]
row.capacity = headers.size
cols.each |k| { row.add(map[k] ?: "") }
rows.add(row)
}
return
}
// scalar value
headers = ["val"]
rows = [[str(x)]]
}
Str[] headers := [,]
Str[][] rows := [,]
once Int[] widths()
{
widths := Int[,]
widths.capacity = headers.size
headers.each |h, c|
{
w := h.size
rows.each |row|
{
w = w.max(row[c].size)
}
widths.add(w)
}
return widths
}
Void dump(Console c)
{
c.info(row(headers))
c.info(underlines)
rows.each |x| { c.info(row(x)) }
}
private Str row(Str[] cells)
{
s := StrBuf()
cells.each |cell, i|
{
if (i > 0) s.add(" ")
s.add(cell)
s.add(Str.spaces(widths[i] - cell.size))
}
return s.toStr
}
private Str underlines()
{
s := StrBuf()
headers.each |h, i|
{
if (i > 0) s.add(" ")
widths[i].times { s.addChar('-') }
}
return s.toStr
}
static Str:Str map(Obj? x)
{
if (x == null) return ["val":"null"]
m := x.typeof.method("each", false)
if (m == null || x is Str) return ["val":str(x)]
acc := Str:Str[:] { ordered = true }
f := |v, k| { acc[str(k)] = str(v)}
m.callOn(x, [f])
return acc
}
private static Str str(Obj? x)
{
if (x == null) return "null"
s := x.toStr
if (s.contains("\n")) s = s.splitLines.first
if (s.size > 80) s = s[0..80] + ".."
return s
}
}