/*
* Copyright 1995, 2016 Perforce Software. All rights reserved.
*
* This file is part of Perforce - the FAST SCM System.
*/
# define NEED_FILE
# define NEED_WINDOWSH
# include <stdhdrs.h>
# include <strbuf.h>
# include <strdict.h>
# include <vararray.h>
# include <strtable.h>
# include <strops.h>
# include <enviro.h>
# include <error.h>
# include <debug.h>
# include <filesys.h>
# include <maptable.h>
# include <maphalf.h>
# include <mapitem.h>
# include <msgclient.h>
# include <msgserver.h>
# include "clientuser.h"
# include "clientusercolor.h"
/*
* Setup ANSI color coding constants
* Mostly Windows specific mappings
*/
const int ESC = '\033';
const StrRef csi( "\033[" );
const StrRef r( "0" );
const StrRef m( "m" );
# ifdef OS_NT
# ifdef OS_MINGW
#define COMMON_LVB_UNDERSCORE 0x8000
# endif
int WinColors[] = {
0, // 30,40 Black
4, // 31,41 Red
2, // 32,42 Green
6, // 33,43 Yellow
1, // 34,44 Blue
5, // 35,45 Magenta
3, // 36,46 Cyan
7 // 37,47 Gray
} ;
int Types[] = {
0, // Reset/Normal
FOREGROUND_INTENSITY, // Bold
0, // Faint (noop)
0, // Italic (noop)
COMMON_LVB_UNDERSCORE // Underline
} ;
#endif
static void
FixWildcards( StrBuf &o )
{
StrBuf tmp;
StrOps::Replace( tmp, o, StrRef("*"), StrRef("...") );
if( !tmp.Contains( StrRef( "...." ) ) )
{
o = tmp;
return;
}
o.Clear();
int d = 0, l = 0, i = 0;
for( ; i < tmp.Length(); i++ )
{
if( tmp.Text()[i] == '.' )
d++;
if( tmp.Text()[i] == '.' && d == 3 && i != l + d - 3 )
{
o.Append( tmp.Text() + l, i - l + 1 );
l = i;
}
else if( tmp.Text()[i] != '.' )
{
if( d >= 3 )
l = i;
d = 0;
}
}
if( i - l > 0 && d < 3 )
o.Append( tmp.Text() + l, i - l + 1 );
}
/*
* ClientUserColor -- version of output that displays ANSI colors
*/
ClientUserColor::ClientUserColor( int forceColors, int autoLoginPrompt ) :
ClientUser( autoLoginPrompt )
{
quiet = 0;
colors = 0;
conditionals = 0;
outColor = 0;
errColor = 0;
#ifdef OS_NT
// Windows check for console
// We also need to stash the console handles and original colors
hConsoleOut = GetStdHandle( STD_OUTPUT_HANDLE );
hConsoleErr = GetStdHandle( STD_ERROR_HANDLE );
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(hConsoleOut, &info);
oldColors = info.wAttributes;
#endif
// Check for TTY (or force)
if( forceColors )
{
outColor = 1;
errColor = 1;
return;
}
if( isatty( fileno( stdout ) ) )
outColor = 1;
if( isatty( fileno( stderr ) ) )
errColor = 1;
}
ClientUserColor::~ClientUserColor()
{
if( colors )
delete colors;
if( conditionals )
delete conditionals;
if( outColor )
ResetColor( stdout );
if( errColor )
ResetColor( stderr );
#ifdef OS_NT
fflush( stdout );
fflush( stderr );
CloseHandle( hConsoleOut );
CloseHandle( hConsoleErr );
#endif
}
void
ClientUserColor::HandleError( Error *err )
{
StrBuf buf;
if( ColorMessage( err, errColor, buf, EF_NEWLINE ) )
OutputAnsiStream( buf.Text(), stderr );
else
OutputError( buf.Text() );
}
void
ClientUserColor::Message( Error *err )
{
int keepfile = 0;
if( err->IsInfo() )
{
// Info
StrBuf buf;
if( ColorMessage( err, outColor, buf, EF_PLAIN ) )
OutputAnsiInfo( (char) err->GetGeneric() + '0', buf.Text() );
else
OutputInfo( (char) err->GetGeneric() + '0', buf.Text() );
if( err->CheckId( MsgServer::SpecNotCorrect ) )
keepfile = 1;
}
else
{
// warn, failed, fatal
HandleError( err );
// report file name left
if( !err->CheckId( MsgServer::ErrorInSpec ) )
keepfile = 1;
}
if( editFile.Length() > 0 )
{
if( keepfile )
{
Error other;
other.Set( MsgClient::FileKept ) << editFile.Text();
HandleError( &other );
}
else
{
FileSys *f = File( FST_UNICODE );
f->Set( editFile );
f->Unlink( err );
delete f;
}
editFile.Clear();
}
}
void
ClientUserColor::SetQuiet()
{
quiet = 1;
ClientUser::SetQuiet();
}
void
ClientUserColor::OutputAnsiInfo( char level, const char *data )
{
if( quiet )
return;
switch( level )
{
default:
case '0': break;
case '1': printf( "... " ); break;
case '2': printf( "... ... " ); break;
}
OutputAnsiStream( data, stdout );
fputc( '\n', stdout );
}
int
ClientUserColor::ColorMessage( Error *err, int color, StrBuf &out, int flag )
{
StrRef var, val;
StrPtr *b = 0;
StrBuf buf;
MapItem *mi;
StrDict* tags = err->GetDict();
MapTable colors;
if( !color || !GetColors( tags, colors ) )
{
err->Fmt( out, flag );
return 0;
}
buf << "@" << err->FmtSeverity();
if( ( mi = colors.Check( LHS, buf ) ) )
b = mi->Rhs();
buf.Clear();
StrBufDict ctags;
int i = 0;
while( tags && tags->GetVar( i++, var, val ) )
{
if( ( mi = colors.Check( LHS, var ) ) )
{
buf.Clear();
buf << csi << mi->Rhs() << m << val << csi << ( b ? b : &r ) << m;
ctags.SetVar( var, buf );
}
else
ctags.SetVar( var, val );
}
buf.Clear();
FmtError( buf, err, flag, &ctags );
if( b )
out << csi << b << m << buf << csi << r << m;
else
out << buf;
return 1;
}
void
ClientUserColor::FmtError( StrBuf &buf, Error *err, int opts, StrDict *tags )
{
if( !err->GetSeverity() )
return;
if( !err->IsInfo() )
buf.Clear();
StrBuf lfmt;
StrPtr *l = 0;
if( !( opts & EF_NOXLATE ) )
{
lfmt.Set( "lfmt" );
l = &lfmt;
}
for( int i = err->GetErrorCount(); i-- > 0; )
{
if( opts & EF_CODE )
{
buf << StrNum( err->GetId( i )->UniqueCode() );
buf.Extend( ':' );
}
if( opts & EF_INDENT ) buf.Append( "\t", 1 );
StrPtr *s;
StrRef fmt;
if( !l || !( s = tags->GetVar( *l, i ) ) )
{
fmt.Set( (char *)err->GetId( i )->fmt );
s = &fmt;
}
StrOps::Expand2( buf, *s, *tags );
// Always insert; sometimes append
if( i || opts & EF_NEWLINE )
buf.Append( "\n", 1 );
}
}
void
ClientUserColor::OutputStream( const char *text, int length, FILE *stream )
{
# ifdef OS_VMS
// Note - VMS likes the args this way, Mac the other,
// UNIX doesn't care.
//
fwrite( text, length, 1, stream );
# else
fwrite( text, 1, length, stream );
# endif
}
void
ClientUserColor::SetColor( int type, int fg, int bg, FILE *stream )
{
#ifdef OS_NT
short fcolor = 0;
short bcolor = 0;
short newColors = 0;
if( fg )
fcolor = WinColors[fg - 30];
if( bg )
bcolor = WinColors[bg - 40] * 16;
newColors = fcolor + bcolor + Types[type];
if( stream == stdout )
SetConsoleTextAttribute( hConsoleOut, newColors );
else
SetConsoleTextAttribute( hConsoleErr, newColors );
#else
char buf[25];
int len = 0;
len = sprintf( buf, "\033[%d", type );
if( fg )
len += sprintf( buf + len, ";%d", fg );
if( bg )
len += sprintf( buf + len, ";%d", bg );
len += sprintf( buf + len, "m" );
OutputStream( buf, len, stream );
#endif
}
void
ClientUserColor::ResetColor( FILE *stream )
{
#ifdef OS_NT
if( stream == stdout )
SetConsoleTextAttribute( hConsoleOut, oldColors );
else
SetConsoleTextAttribute( hConsoleErr, oldColors );
#else
OutputStream( "\033[0m", 4, stream );
#endif
}
void
ClientUserColor::OutputAnsiStream( const char *text, FILE *stream )
{
fflush( stream );
short oldColors = 0;
void *hConsole = 0;
const char *p;
// We already know whether we can render colors or not, but we still
// iterate over the string so that any characters that
while( ( p = strchr( text, ESC ) ) )
{
int length = strlen( text );
// Print stuff before the maker
if( p > text )
OutputStream( text, p - text, stream );
// Check for the reset marker (easy)
if( ( p - text + 4 ) <= length && !strncmp( p + 1, "[0m", 3 ) )
{
text = p += 4;
ResetColor( stream );
continue;
}
int type = 0;
int fg = 0;
int bg = 0;
int temp = 0;
// Jump over the CSI ^[[
if( ( p - text + 2 ) <= length && p[1] == '[' )
p += 2;
// Loop over the string until we reach a terminator
while( ( p - text ) <= length )
if( *p - '0' >= 0 && *p - '0' <= 9 ) // Interpret digit
temp = ( temp * 10 ) + ( *p++ - '0' );
else if( *p == ';' || ( *p >= 64 && *p <= 126 ) )
{
// Convert to meaning
if( temp >= 0 && temp <= 4 )
type = temp;
else if( temp >= 30 && temp <= 37 )
fg = temp;
else if( temp >= 40 && temp <= 47 )
bg = temp;
if( *p == 'm' )
{
// Apply color
SetColor( type, fg, bg, stream );
p++;
break;
}
else if( *p >= 64 && *p <= 126 )
{
// End of sequence (no color application)
p++;
break;
}
temp = 0;
p++;
}
else
p++;
text = p;
}
// Print anything after the last marker
OutputStream( text, strlen( text ), stream );
// Back to default
ResetColor( stream );
}
StrDict *
ClientUserColor::GetColors()
{
char *c = 0, *c0;
StrRef var, val;
if( colors && colors->GetVar( 0, var, val ) )
{
// OK.
return colors;
}
else if ( ( c = enviro->Get( "P4COLORS" ) ) )
{
if( !colors )
colors = new StrBufDict;
if( !conditionals )
conditionals = new MapTable;
char *l = c + strlen( c );
char *s = 0, *e, *q, *p, *r = 0;
StrBuf var1, val1, val2, patten;
StrBufDict tmpColors;
bool ingroup = false;
/*
* Colors come in three types.
*
* 1. Static:
*
* var=color:
* c e s
*
* 2. Conditional:
*
* (var=val)var=color:
* c p qc e s
*
* 2. Conditional-multi:
*
* (var=val)[var=color:var=color]:
* c p qcc e sc e rs
*
* Notes:
* c is promoted during condition parse
* the trailing : may not exist for the last statement
*/
while( c <= l &&
( ( s = strchr( c, ':' ) ) ||
( strlen( c ) && ( s = strlen( c ) + c ) ) ) )
{
q = 0;
p = 0;
if( !r )
patten.Clear();
// Check if the value is a conditional
if( *c == '(' )
{
c0 = c;
// find the end of the condition, or skip
if( !( q = strchr( c0, ')' ) ) || q >= s )
{
c = s + 1;
continue;
}
c = q + 1;
// there must be a separator inside the condition
if( !( p = strchr( c0, '=' ) ) || p >= q )
{
q = 0;
p = 0;
}
if( q )
{
// Save the matcher pattern
patten.Set( c0 + 1, q - c0 - 1 );
FixWildcards( patten );
}
else
{
c = s + 1;
continue;
}
}
// Check if this is a group of values (skip [ within [])
if( r && c < r && *c == '[' )
c++;
if( *c == '[' && ( r = strchr( ++c, ']' ) ) )
{
// Cant have conditionals in groups
if( ( q = strchr( c, '(' ) ) && q <= r )
{
c0 = strchr( r, ':' );
if( !c0 )
c0 = strlen( c ) + c;
c = c0;
continue;
}
tmpColors.Clear();
}
// Make sure there's an = before the :, otherwise skip
if( !( e = strchr( c, '=' ) ) || e >= s )
{
if( r && r < s )
{
// Persist the group
val1.Clear();
int i = 0;
if( patten.Length() )
{
while( tmpColors.GetVar( i++, var, val ) )
val1 << var << "=" << val << ":";
conditionals->Insert( patten, val1 );
}
else
while( tmpColors.GetVar( i++, var, val ) )
colors->SetVar( var, val );
r = 0;
}
c = s + 1;
continue;
}
if( r && r < s )
s = r;
// Get the tag pattern
var1.Set( c, e - c );
FixWildcards( var1 );
// Get the value
val1.Set( e + 1, s - e - 1 );
for( int i = 0; i < val1.Length(); i++ )
if( val1.Text()[i] != ';' &&
!( val1.Text()[i] >= '0' &&
val1.Text()[i] <= '9' ) )
{
// Invalid value, skip it by unsetting the var
var1.SetLength( 0 );
var1.Terminate();
break;
}
StrOps::Replace( val2, val1, StrRef("00"), StrRef("0") );
val1.Clear();
val1 << var1.Text() << "=" << val2.Text();
if( r && var1.Length() && val2.Length() )
tmpColors.SetVar( var1, val2 );
else if( patten.Length() && var1.Length() && val2.Length() )
conditionals->Insert( patten, val1 );
else if( var1.Length() && val2.Length() )
colors->SetVar( var1, val2 );
if( r && r <= s )
{
// Persist the group
val1.Clear();
int i = 0;
if( patten.Length() )
{
while( tmpColors.GetVar( i++, var, val ) )
val1 << var.Text() << "=" << val.Text() << ":";
if( val1.Length() )
conditionals->Insert( patten, val1 );
}
else
while( tmpColors.GetVar( i++, var, val ) )
colors->SetVar( var, val );
r = 0;
}
c = s + 1;
}
}
else
{
if( !colors )
colors = new StrBufDict;
else
colors->Clear();
if( !conditionals )
conditionals = new MapTable;
else
conditionals->Clear();
}
return colors;
}
int
ClientUserColor::GetColors( StrDict *tags, MapTable &c )
{
int count = 0, i = 0;
StrRef var, val;
StrBuf tmp;
StrBufDict d;
// Fill the new dict
GetColors();
while( colors && colors->GetVar( count++, var, val ) )
d.SetVar( var, val );
StrBuf match;
MapItem *mi;
while( tags && tags->GetVar( i++, var, val ) )
{
match.Clear();
match << var << "=" << val;
if( ( mi = conditionals->Check( LHS, match ) ) )
{
char *s = mi->Rhs()->Text();
char *l = mi->Rhs()->Text() + mi->Rhs()->Length();
char *q = 0, *p;
StrBuf var1, val1;
while( s < l &&
( ( q = strchr( s, ':' ) ) ||
( strlen( s ) && ( q = strlen( s ) + s ) ) ) )
{
p = strchr( s, '=' );
if( !p && s++ )
continue;
var1.Set( s, p - s );
val1.Set( p + 1, q - p - 1 );
if( !d.GetVar( var1 ) )
d.SetVar( var1, val1 );
else
d.ReplaceVar( var1, val1 );
s = q + 1;
}
}
}
count = 0;
while( d.GetVar( count++, var, val ) )
c.Insert( var, val );
return count;
}