fish-shell/reader.cpp

3492 lines
66 KiB
C++

/** \file reader.c
Functions for reading data from stdin and passing to the
parser. If stdin is a keyboard, it supplies a killring, history,
syntax highlighting, tab-completion and various other interactive features.
Internally the interactive mode functions rely in the functions of the
input library to read individual characters of input.
Token search is handled incrementally. Actual searches are only done
on when searching backwards, since the previous results are saved. The
last search position is remembered and a new search continues from the
last search position. All search results are saved in the list
'search_prev'. When the user searches forward, i.e. presses Alt-down,
the list is consulted for previous search result, and subsequent
backwards searches are also handled by consultiung the list up until
the end of the list is reached, at which point regular searching will
commence.
*/
#include "config.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_SYS_TERMIOS_H
#include <sys/termios.h>
#endif
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#include <time.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/poll.h>
#include <unistd.h>
#include <wctype.h>
#if HAVE_NCURSES_H
#include <ncurses.h>
#else
#include <curses.h>
#endif
#if HAVE_TERMIO_H
#include <termio.h>
#endif
#if HAVE_TERM_H
#include <term.h>
#elif HAVE_NCURSES_TERM_H
#include <ncurses/term.h>
#endif
#ifdef HAVE_SIGINFO_H
#include <siginfo.h>
#endif
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#include <signal.h>
#include <fcntl.h>
#include <dirent.h>
#include <wchar.h>
#include <assert.h>
#include "fallback.h"
#include "util.h"
#include "wutil.h"
#include "highlight.h"
#include "reader.h"
#include "proc.h"
#include "parser.h"
#include "complete.h"
#include "history.h"
#include "common.h"
#include "sanity.h"
#include "env.h"
#include "exec.h"
#include "expand.h"
#include "tokenizer.h"
#include "kill.h"
#include "input_common.h"
#include "input.h"
#include "function.h"
#include "output.h"
#include "signal.h"
#include "screen.h"
#include "halloc.h"
#include "halloc_util.h"
#include "iothread.h"
#include "parse_util.h"
/**
Maximum length of prefix string when printing completion
list. Longer prefixes will be ellipsized.
*/
#define PREFIX_MAX_LEN 8
/**
A simple prompt for reading shell commands that does not rely on
fish specific commands, meaning it will work even if fish is not
installed. This is used by read_i.
*/
#define DEFAULT_PROMPT L"echo \"$USER@\"; hostname|cut -d . -f 1; echo \" \"; pwd; printf '> ';"
/**
The name of the function that prints the fish prompt
*/
#define PROMPT_FUNCTION_NAME L"fish_prompt"
/**
The default title for the reader. This is used by reader_readline.
*/
#define DEFAULT_TITLE L"echo $_ \" \"; pwd"
/**
The maximum number of characters to read from the keyboard without
repainting. Note that this readahead will only occur if new
characters are avaialble for reading, fish will never block for
more input without repainting.
*/
#define READAHEAD_MAX 256
/**
A mode for calling the reader_kill function. In this mode, the new
string is appended to the current contents of the kill buffer.
*/
#define KILL_APPEND 0
/**
A mode for calling the reader_kill function. In this mode, the new
string is prepended to the current contents of the kill buffer.
*/
#define KILL_PREPEND 1
/**
History search mode. This value means that no search is currently
performed.
*/
#define NO_SEARCH 0
/**
History search mode. This value means that we are perforing a line
history search.
*/
#define LINE_SEARCH 1
/**
History search mode. This value means that we are perforing a token
history search.
*/
#define TOKEN_SEARCH 2
/**
History search mode. This value means we are searching backwards.
*/
#define SEARCH_BACKWARD 0
/**
History search mode. This value means we are searching forwards.
*/
#define SEARCH_FORWARD 1
/**
A struct describing the state of the interactive reader. These
states can be stacked, in case reader_readline() calls are
nested. This happens when the 'read' builtin is used.
*/
typedef struct reader_data
{
/**
Buffer containing the whole current commandline
*/
wchar_t *buff;
/**
The representation of the current screen contents
*/
screen_t screen;
/**
Buffer containing the current search item
*/
string_buffer_t search_buff;
/**
Saved position used by token history search
*/
int token_history_pos;
/**
Saved search string for token history search. Not handled by check_size.
*/
const wchar_t *token_history_buff;
/**
List for storing previous search results. Used to avoid duplicates.
*/
array_list_t search_prev;
/**
The current position in search_prev
*/
int search_pos;
/**
Current size of the buffers
*/
size_t buff_sz;
/**
Length of the command in buff. (Not the length of buff itself)
*/
size_t buff_len;
/**
The current position of the cursor in buff.
*/
size_t buff_pos;
/**
Name of the current application
*/
wchar_t *name;
/** The prompt command */
wchar_t *prompt;
/** The output of the last evaluation of the prompt command */
string_buffer_t prompt_buff;
/**
Color is the syntax highlighting for buff. The format is that
color[i] is the classification (according to the enum in
highlight.h) of buff[i].
*/
int *color;
/**
An array defining the block level at each character.
*/
int *indent;
/**
Function for tab completion
*/
void (*complete_func)( const wchar_t *,
array_list_t * );
/**
Function for syntax highlighting
*/
highlight_function_t highlight_function;
/**
Function for testing if the string can be returned
*/
int (*test_func)( wchar_t * );
/**
When this is true, the reader will exit
*/
int end_loop;
/**
If this is true, exit reader even if there are running
jobs. This happens if we press e.g. ^D twice.
*/
int prev_end_loop;
/**
The current contents of the top item in the kill ring.
*/
string_buffer_t kill_item;
/**
Pointer to previous reader_data
*/
struct reader_data *next;
/**
This variable keeps state on if we are in search mode, and
if yes, what mode
*/
int search_mode;
/**
Keep track of whether any internal code has done something
which is known to require a repaint.
*/
int repaint_needed;
}
reader_data_t;
/**
The current interactive reading context
*/
static reader_data_t *data=0;
/**
This flag is set to true when fish is interactively reading from
stdin. It changes how a ^C is handled by the fish interrupt
handler.
*/
static int is_interactive_read;
/**
Flag for ending non-interactive shell
*/
static int end_loop = 0;
/**
The list containing names of files that are being parsed
*/
static array_list_t current_filename;
/**
Store the pid of the parent process, so the exit function knows whether it should reset the terminal or not.
*/
static pid_t original_pid;
/**
This variable is set to true by the signal handler when ^C is pressed
*/
static int interrupted=0;
/*
Prototypes for a bunch of functions defined later on.
*/
/**
Stores the previous termios mode so we can reset the modes when
we execute programs and when the shell exits.
*/
static struct termios saved_modes;
static void reader_super_highlight_me_plenty( int pos, array_list_t *error );
/**
Variable to keep track of forced exits - see \c reader_exit_forced();
*/
static int exit_forced;
/**
Give up control of terminal
*/
static void term_donate()
{
set_color(FISH_COLOR_NORMAL, FISH_COLOR_NORMAL);
while( 1 )
{
if( tcsetattr(0,TCSANOW,&saved_modes) )
{
if( errno != EINTR )
{
debug( 1, _( L"Could not set terminal mode for new job" ) );
wperror( L"tcsetattr" );
break;
}
}
else
break;
}
}
/**
Grab control of terminal
*/
static void term_steal()
{
while( 1 )
{
if( tcsetattr(0,TCSANOW,&shell_modes) )
{
if( errno != EINTR )
{
debug( 1, _( L"Could not set terminal mode for shell" ) );
wperror( L"tcsetattr" );
break;
}
}
else
break;
}
common_handle_winch(0 );
}
int reader_exit_forced()
{
return exit_forced;
}
/**
Repaint the entire commandline. This means reset and clear the
commandline, write the prompt, perform syntax highlighting, write
the commandline and move the cursor.
*/
static void reader_repaint()
{
parser_test( data->buff, data->indent, 0, 0 );
s_write( &data->screen,
(wchar_t *)data->prompt_buff.buff,
data->buff,
data->color,
data->indent,
data->buff_pos );
data->repaint_needed = 0;
}
/**
Internal helper function for handling killing parts of text.
*/
static void reader_kill( wchar_t *begin, int length, int mode, int newv )
{
if( newv )
{
sb_clear( &data->kill_item );
sb_append_substring( &data->kill_item, begin, length );
kill_add( (wchar_t *)data->kill_item.buff );
}
else
{
wchar_t *old = wcsdup( (wchar_t *)data->kill_item.buff);
if( mode == KILL_APPEND )
{
sb_append_substring( &data->kill_item, begin, length );
}
else
{
sb_clear( &data->kill_item );
sb_append_substring( &data->kill_item, begin, length );
sb_append( &data->kill_item, old );
}
kill_replace( old, (wchar_t *)data->kill_item.buff );
free( old );
}
if( data->buff_pos > (begin-data->buff) )
{
data->buff_pos = maxi( begin-data->buff, data->buff_pos-length );
}
data->buff_len -= length;
memmove( begin, begin+length, sizeof( wchar_t )*(wcslen( begin+length )+1) );
reader_super_highlight_me_plenty( data->buff_pos, 0 );
reader_repaint();
}
void reader_handle_int( int sig )
{
block_t *c = current_block;
if( !is_interactive_read )
{
while( c )
{
c->type=FAKE;
c->skip=1;
c=c->outer;
}
}
interrupted = 1;
}
wchar_t *reader_current_filename()
{
return al_get_count( &current_filename )?(wchar_t *)al_peek( &current_filename ):0;
}
void reader_push_current_filename( const wchar_t *fn )
{
al_push( &current_filename, fn );
}
wchar_t *reader_pop_current_filename()
{
return (wchar_t *)al_pop( &current_filename );
}
/**
Make sure buffers are large enough to hold current data plus one extra character.
*/
static int check_size()
{
if( data->buff_sz < data->buff_len + 2 )
{
data->buff_sz = maxi( 128, data->buff_len*2 );
data->buff = (wchar_t *)realloc( data->buff,
sizeof(wchar_t)*data->buff_sz);
data->color = (int *)realloc( data->color,
sizeof(int)*data->buff_sz);
data->indent = (int *)realloc( data->indent,
sizeof(int)*data->buff_sz);
if( data->buff==0 ||
data->color==0 ||
data->indent == 0 )
{
DIE_MEM();
}
}
return 1;
}
/**
Compare two completion entrys
*/
static int completion_cmp( const void *a, const void *b )
{
completion_t *c= *((completion_t **)a);
completion_t *d= *((completion_t **)b);
return wcsfilecmp( c->completion, d->completion );
}
/**
Sort an array_list_t containing compltion_t structs.
*/
static void sort_completion_list( array_list_t *comp )
{
qsort( comp->arr,
al_get_count( comp ),
sizeof( void*),
&completion_cmp );
}
/**
Remove any duplicate completions in the list. This relies on the
list first beeing sorted.
*/
static void remove_duplicates( array_list_t *l )
{
int in, out;
const wchar_t *prev;
completion_t *first;
if( al_get_count( l ) == 0 )
return;
first = (completion_t *)al_get( l, 0 );
prev = first->completion;
for( in=1, out=1; in < al_get_count( l ); in++ )
{
completion_t *curr = (completion_t *)al_get( l, in );
if( wcscmp( prev, curr->completion )!=0 )
{
al_set( l, out++, curr );
}
prev = curr->completion;
}
al_truncate( l, out );
}
int reader_interrupted()
{
int res=interrupted;
if( res )
interrupted=0;
return res;
}
void reader_write_title()
{
const wchar_t *title;
array_list_t l;
wchar_t *term = env_get( L"TERM" );
/*
This is a pretty lame heuristic for detecting terminals that do
not support setting the title. If we recognise the terminal name
as that of a virtual terminal, we assume it supports setting the
title. If we recognise it as that of a console, we assume it
does not support setting the title. Otherwise we check the
ttyname and see if we belive it is a virtual terminal.
One situation in which this breaks down is with screen, since
screen supports setting the terminal title if the underlying
terminal does so, but will print garbage on terminals that
don't. Since we can't see the underlying terminal below screen
there is no way to fix this.
*/
if ( !term )
{
return;
}
if( !contains( term, L"xterm", L"screen", L"nxterm", L"rxvt" ) )
{
char *n = ttyname( STDIN_FILENO );
if( contains( term, L"linux" ) )
{
return;
}
if( strstr( n, "tty" ) || strstr( n, "/vc/") )
return;
}
title = function_exists( L"fish_title" )?L"fish_title":DEFAULT_TITLE;
if( wcslen( title ) ==0 )
return;
al_init( &l );
proc_push_interactive(0);
if( exec_subshell( title, &l ) != -1 )
{
int i;
if( al_get_count( &l ) > 0 )
{
writestr( L"\x1b]2;" );
for( i=0; i<al_get_count( &l ); i++ )
{
writestr( (wchar_t *)al_get( &l, i ) );
}
writestr( L"\7" );
}
}
proc_pop_interactive();
al_foreach( &l, &free );
al_destroy( &l );
set_color( FISH_COLOR_RESET, FISH_COLOR_RESET );
}
/**
Reexecute the prompt command. The output is inserted into data->prompt_buff.
*/
static void exec_prompt()
{
int i;
array_list_t prompt_list;
al_init( &prompt_list );
if( data->prompt )
{
proc_push_interactive( 0 );
if( exec_subshell( data->prompt, &prompt_list ) == -1 )
{
/* If executing the prompt fails, make sure we at least don't print any junk */
al_foreach( &prompt_list, &free );
al_destroy( &prompt_list );
al_init( &prompt_list );
}
proc_pop_interactive();
}
reader_write_title();
sb_clear( &data->prompt_buff );
for( i = 0; i < al_get_count( &prompt_list )-1; i++ )
{
sb_append( &data->prompt_buff, (wchar_t *)al_get( &prompt_list, i ), L"\n" );
}
sb_append( &data->prompt_buff, (wchar_t *)al_get( &prompt_list, i ));
al_foreach( &prompt_list, &free );
al_destroy( &prompt_list );
}
void reader_init()
{
tcgetattr(0,&shell_modes); /* get the current terminal modes */
memcpy( &saved_modes,
&shell_modes,
sizeof(saved_modes)); /* save a copy so we can reset the terminal later */
shell_modes.c_lflag &= ~ICANON; /* turn off canonical mode */
shell_modes.c_lflag &= ~ECHO; /* turn off echo mode */
shell_modes.c_cc[VMIN]=1;
shell_modes.c_cc[VTIME]=0;
al_init( &current_filename);
}
void reader_destroy()
{
al_destroy( &current_filename);
tcsetattr(0, TCSANOW, &saved_modes);
}
void reader_exit( int do_exit, int forced )
{
if( data )
data->end_loop=do_exit;
end_loop=do_exit;
if( forced )
exit_forced = 1;
}
void reader_repaint_needed()
{
if( data )
{
data->repaint_needed = 1;
}
}
/**
Remove the previous character in the character buffer and on the
screen using syntax highlighting, etc.
*/
static void remove_backward()
{
if( data->buff_pos <= 0 )
return;
if( data->buff_pos < data->buff_len )
{
memmove( &data->buff[data->buff_pos-1],
&data->buff[data->buff_pos],
sizeof(wchar_t)*(data->buff_len-data->buff_pos+1) );
}
data->buff_pos--;
data->buff_len--;
data->buff[data->buff_len]=0;
reader_super_highlight_me_plenty( data->buff_pos,
0 );
reader_repaint();
}
/**
Insert the characters of the string into the command line buffer
and print them to the screen using syntax highlighting, etc.
*/
static int insert_str(wchar_t *str)
{
int len = wcslen( str );
int old_len = data->buff_len;
assert( data->buff_pos >= 0 );
assert( data->buff_pos <= data->buff_len );
assert( len >= 0 );
data->buff_len += len;
check_size();
/* Insert space for extra characters at the right position */
if( data->buff_pos < old_len )
{
memmove( &data->buff[data->buff_pos+len],
&data->buff[data->buff_pos],
sizeof(wchar_t)*(old_len-data->buff_pos) );
}
memmove( &data->buff[data->buff_pos], str, sizeof(wchar_t)*len );
data->buff_pos += len;
data->buff[data->buff_len]='\0';
/*
Syntax highlight
*/
reader_super_highlight_me_plenty( data->buff_pos-1,
0 );
reader_repaint();
return 1;
}
/**
Insert the character into the command line buffer and print it to
the screen using syntax highlighting, etc.
*/
static int insert_char( int c )
{
wchar_t str[]=
{
0, 0
}
;
str[0] = c;
return insert_str( str );
}
/**
Calculate the length of the common prefix substring of two strings.
*/
static int comp_len( const wchar_t *a, const wchar_t *b )
{
int i;
for( i=0;
a[i] != '\0' && b[i] != '\0' && a[i]==b[i];
i++ )
;
return i;
}
/**
Calculate the case insensitive length of the common prefix substring of two strings.
*/
static int comp_ilen( const wchar_t *a, const wchar_t *b )
{
int i;
for( i=0;
a[i] != '\0' && b[i] != '\0' && towlower(a[i])==towlower(b[i]);
i++ )
;
return i;
}
/**
Find the outermost quoting style of current token. Returns 0 if
token is not quoted.
*/
static wchar_t get_quote( wchar_t *cmd, int len )
{
int i=0;
wchar_t res=0;
while( 1 )
{
if( !cmd[i] )
break;
if( cmd[i] == L'\\' )
{
i++;
if( !cmd[i] )
break;
i++;
}
else
{
if( cmd[i] == L'\'' || cmd[i] == L'\"' )
{
const wchar_t *end = quote_end( &cmd[i] );
//fwprintf( stderr, L"Jump %d\n", end-cmd );
if(( end == 0 ) || (!*end) || (end-cmd > len))
{
res = cmd[i];
break;
}
i = end-cmd+1;
}
else
i++;
}
}
return res;
}
/**
Calculates information on the parameter at the specified index.
\param cmd The command to be analyzed
\param pos An index in the string which is inside the parameter
\param quote If not 0, store the type of quote this parameter has, can be either ', " or \\0, meaning the string is not quoted.
\param offset If not 0, get_param will store a pointer to the beginning of the parameter.
\param string If not 0, get_parm will store a copy of the parameter string as returned by the tokenizer.
\param type If not 0, get_param will store the token type as returned by tok_last.
*/
static void get_param( wchar_t *cmd,
int pos,
wchar_t *quote,
wchar_t **offset,
wchar_t **string,
int *type )
{
int prev_pos=0;
wchar_t last_quote = '\0';
int unfinished;
tokenizer tok;
tok_init( &tok, cmd, TOK_ACCEPT_UNFINISHED );
for( ; tok_has_next( &tok ); tok_next( &tok ) )
{
if( tok_get_pos( &tok ) > pos )
break;
if( tok_last_type( &tok ) == TOK_STRING )
last_quote = get_quote( tok_last( &tok ),
pos - tok_get_pos( &tok ) );
if( type != 0 )
*type = tok_last_type( &tok );
if( string != 0 )
wcscpy( *string, tok_last( &tok ) );
prev_pos = tok_get_pos( &tok );
}
tok_destroy( &tok );
wchar_t c = cmd[pos];
cmd[pos]=0;
int cmdlen = wcslen( cmd );
unfinished = (cmdlen==0);
if( !unfinished )
{
unfinished = (quote != 0);
if( !unfinished )
{
if( wcschr( L" \t\n\r", cmd[cmdlen-1] ) != 0 )
{
if( ( cmdlen == 1) || (cmd[cmdlen-2] != L'\\') )
{
unfinished=1;
}
}
}
}
if( quote )
*quote = last_quote;
if( offset != 0 )
{
if( !unfinished )
{
while( (cmd[prev_pos] != 0) && (wcschr( L";|",cmd[prev_pos])!= 0) )
prev_pos++;
*offset = cmd + prev_pos;
}
else
{
*offset = cmd + pos;
}
}
cmd[pos]=c;
}
/**
Insert the string at the current cursor position. The function
checks if the string is quoted or not and correctly escapes the
string.
\param val the string to insert
\param flags A union of all flags describing the completion to insert. See the completion_t struct for more information on possible values.
*/
static void completion_insert( const wchar_t *val, int flags )
{
wchar_t *replaced;
wchar_t quote;
int add_space = !(flags & COMPLETE_NO_SPACE);
int do_replace = (flags & COMPLETE_NO_CASE);
int do_escape = !(flags & COMPLETE_DONT_ESCAPE);
// debug( 0, L"Insert completion %ls with flags %d", val, flags);
if( do_replace )
{
int tok_start, tok_len, move_cursor;
wchar_t *begin, *end;
string_buffer_t sb;
wchar_t *escaped;
parse_util_token_extent( data->buff, data->buff_pos, &begin, 0, 0, 0 );
end = data->buff+data->buff_pos;
tok_start = begin - data->buff;
tok_len = end-begin;
sb_init( &sb );
sb_append_substring( &sb, data->buff, begin - data->buff );
if( do_escape )
{
escaped = escape( val, ESCAPE_ALL | ESCAPE_NO_QUOTED );
sb_append( &sb, escaped );
move_cursor = wcslen(escaped);
free( escaped );
}
else
{
sb_append( &sb, val );
move_cursor = wcslen(val);
}
if( add_space )
{
sb_append( &sb, L" " );
move_cursor += 1;
}
sb_append( &sb, end );
reader_set_buffer( (wchar_t *)sb.buff, (begin-data->buff)+move_cursor );
sb_destroy( &sb );
reader_super_highlight_me_plenty( data->buff_pos, 0 );
reader_repaint();
}
else
{
if( do_escape )
{
get_param( data->buff,
data->buff_pos,
&quote,
0, 0, 0 );
if( quote == L'\0' )
{
replaced = escape( val, ESCAPE_ALL | ESCAPE_NO_QUOTED );
}
else
{
int unescapable=0;
const wchar_t *pin;
wchar_t *pout;
replaced = pout =
(wchar_t *)malloc( sizeof(wchar_t)*(wcslen(val) + 1) );
for( pin=val; *pin; pin++ )
{
switch( *pin )
{
case L'\n':
case L'\t':
case L'\b':
case L'\r':
unescapable=1;
break;
default:
*pout++ = *pin;
break;
}
}
if( unescapable )
{
free( replaced );
wchar_t *tmp = escape( val, ESCAPE_ALL | ESCAPE_NO_QUOTED );
replaced = wcsdupcat( L" ", tmp );
free( tmp);
replaced[0]=quote;
}
else
*pout = 0;
}
}
else
{
replaced = wcsdup(val);
}
if( insert_str( replaced ) )
{
/*
Print trailing space since this is the only completion
*/
if( add_space )
{
if( quote &&
(data->buff[data->buff_pos] != quote ) )
{
/*
This is a quoted parameter, first print a quote
*/
insert_char( quote );
}
insert_char( L' ' );
}
}
free(replaced);
}
}
/**
Run the fish_pager command to display the completion list. If the
fish_pager outputs any text, it is inserted into the input
backbuffer.
\param prefix the string to display before every completion.
\param is_quoted should be set if the argument is quoted. This will change the display style.
\param comp the list of completions to display
*/
static void run_pager( wchar_t *prefix, int is_quoted, array_list_t *comp )
{
int i;
string_buffer_t cmd;
string_buffer_t msg;
wchar_t * prefix_esc;
char *foo;
io_data_t *in;
wchar_t *escaped_separator;
int has_case_sensitive=0;
if( !prefix || (wcslen(prefix)==0))
{
prefix_esc = wcsdup(L"\"\"");
}
else
{
prefix_esc = escape( prefix,1);
}
sb_init( &cmd );
sb_init( &msg );
sb_printf( &cmd,
L"fish_pager -c 3 -r 4 %ls -p %ls",
// L"valgrind --track-fds=yes --log-file=pager.txt --leak-check=full ./fish_pager %d %ls",
is_quoted?L"-q":L"",
prefix_esc );
free( prefix_esc );
in= io_buffer_create( 1 );
in->fd = 3;
escaped_separator = escape( COMPLETE_SEP_STR, 1);
for( i=0; i<al_get_count( comp ); i++ )
{
completion_t *el = (completion_t *)al_get( comp, i );
has_case_sensitive |= !(el->flags & COMPLETE_NO_CASE );
}
for( i=0; i<al_get_count( comp ); i++ )
{
int base_len=-1;
completion_t *el = (completion_t *)al_get( comp, i );
wchar_t *foo=0;
wchar_t *baz=0;
if( has_case_sensitive && (el->flags & COMPLETE_NO_CASE ))
{
continue;
}
if( el && el->completion )
{
if( el->flags & COMPLETE_NO_CASE )
{
if( base_len == -1 )
{
wchar_t *begin;
parse_util_token_extent( data->buff, data->buff_pos, &begin, 0, 0, 0 );
base_len = data->buff_pos - (begin-data->buff);
}
foo = escape( el->completion + base_len, ESCAPE_ALL | ESCAPE_NO_QUOTED );
}
else
{
foo = escape( el->completion, ESCAPE_ALL | ESCAPE_NO_QUOTED );
}
}
if( el && el->description )
{
baz = escape( el->description, 1 );
}
if( !foo )
{
debug( 0, L"Run pager called with bad argument." );
bugreport();
show_stackframe();
}
else if( baz )
{
sb_printf( &msg, L"%ls%ls%ls\n",
foo,
escaped_separator,
baz );
}
else
{
sb_printf( &msg, L"%ls\n",
foo );
}
free( foo );
free( baz );
}
free( escaped_separator );
foo = wcs2str( (wchar_t *)msg.buff );
b_append( in->param2.out_buffer, foo, strlen(foo) );
free( foo );
term_donate();
io_data_t *out = io_buffer_create( 0 );
out->next = in;
out->fd = 4;
eval( (wchar_t *)cmd.buff, out, TOP);
term_steal();
io_buffer_read( out );
sb_destroy( &cmd );
sb_destroy( &msg );
int nil=0;
b_append( out->param2.out_buffer, &nil, 1 );
wchar_t *tmp;
wchar_t *str = str2wcs((char *)out->param2.out_buffer->buff);
if( str )
{
for( tmp = str + wcslen(str)-1; tmp >= str; tmp-- )
{
input_unreadch( *tmp );
}
free( str );
}
io_buffer_destroy( out);
io_buffer_destroy( in);
}
/**
Flash the screen. This function only changed the color of the
current line, since the flash_screen sequnce is rather painful to
look at in most terminal emulators.
*/
static void reader_flash()
{
struct timespec pollint;
int i;
for( i=0; i<data->buff_pos; i++ )
{
data->color[i] = HIGHLIGHT_SEARCH_MATCH<<16;
}
reader_repaint();
pollint.tv_sec = 0;
pollint.tv_nsec = 100 * 1000000;
nanosleep( &pollint, NULL );
reader_super_highlight_me_plenty( data->buff_pos, 0 );
reader_repaint();
}
/**
Characters that may not be part of a token that is to be replaced
by a case insensitive completion.
*/
#define REPLACE_UNCLEAN L"$*?({})"
/**
Check if the specified string can be replaced by a case insensitive
complition with the specified flags.
Advanced tokens like those containing {}-style expansion can not at
the moment be replaced, other than if the new token is already an
exact replacement, e.g. if the COMPLETE_DONT_ESCAPE flag is set.
*/
int reader_can_replace( const wchar_t *in, int flags )
{
const wchar_t * str = in;
if( flags & COMPLETE_DONT_ESCAPE )
{
return 1;
}
CHECK( in, 1 );
/*
Test characters that have a special meaning in any character position
*/
while( *str )
{
if( wcschr( REPLACE_UNCLEAN, *str ) )
return 0;
str++;
}
return 1;
}
/**
Handle the list of completions. This means the following:
- If the list is empty, flash the terminal.
- If the list contains one element, write the whole element, and if
the element does not end on a '/', '@', ':', or a '=', also write a trailing
space.
- If the list contains multiple elements with a common prefix, write
the prefix.
- If the list contains multiple elements without.
a common prefix, call run_pager to display a list of completions. Depending on terminal size and the length of the list, run_pager may either show less than a screenfull and exit or use an interactive pager to allow the user to scroll through the completions.
\param comp the list of completion strings
*/
static int handle_completions( array_list_t *comp )
{
int i;
void *context = 0;
wchar_t *base = 0;
int len = 0;
int done = 0;
int count = 0;
int flags=0;
wchar_t *begin, *end;
wchar_t *tok;
parse_util_token_extent( data->buff, data->buff_pos, &begin, 0, 0, 0 );
end = data->buff+data->buff_pos;
context = halloc( 0, 0 );
tok = halloc_wcsndup( context, begin, end-begin );
/*
Check trivial cases
*/
switch( al_get_count( comp ) )
{
/*
No suitable completions found, flash screen and retur
*/
case 0:
{
reader_flash();
done = 1;
break;
}
/*
Exactly one suitable completion found - insert it
*/
case 1:
{
completion_t *c = (completion_t *)al_get( comp, 0 );
/*
If this is a replacement completion, check
that we know how to replace it, e.g. that
the token doesn't contain evil operators
like {}
*/
if( !(c->flags & COMPLETE_NO_CASE) || reader_can_replace( tok, c->flags ) )
{
completion_insert( c->completion,
c->flags );
}
done = 1;
len = 1;
break;
}
}
if( !done )
{
/*
Try to find something to insert whith the correct case
*/
for( i=0; i<al_get_count( comp ); i++ )
{
completion_t *c = (completion_t *)al_get( comp, i );
int new_len;
/*
Ignore case insensitive completions for now
*/
if( c->flags & COMPLETE_NO_CASE )
continue;
count++;
if( base )
{
new_len = comp_len( base, c->completion );
len = new_len < len ? new_len: len;
}
else
{
base = wcsdup( c->completion );
len = wcslen( base );
flags = c->flags;
}
}
/*
If we found something to insert, do it.
*/
if( len > 0 )
{
if( count > 1 )
flags = flags | COMPLETE_NO_SPACE;
base[len]=L'\0';
completion_insert(base, flags);
done = 1;
}
}
if( !done && base == 0 )
{
/*
Try to find something to insert ignoring case
*/
if( begin )
{
int offset = wcslen( tok );
count = 0;
for( i=0; i<al_get_count( comp ); i++ )
{
completion_t *c = (completion_t *)al_get( comp, i );
int new_len;
if( !(c->flags & COMPLETE_NO_CASE) )
continue;
if( !reader_can_replace( tok, c->flags ) )
{
len=0;
break;
}
count++;
if( base )
{
new_len = offset + comp_ilen( base+offset, c->completion+offset );
len = new_len < len ? new_len: len;
}
else
{
base = wcsdup( c->completion );
len = wcslen( base );
flags = c->flags;
}
}
if( len > offset )
{
if( count > 1 )
flags = flags | COMPLETE_NO_SPACE;
base[len]=L'\0';
completion_insert( base, flags );
done = 1;
}
}
}
free( base );
if( !done )
{
/*
There is no common prefix in the completions, and show_list
is true, so we print the list
*/
int len;
wchar_t * prefix;
wchar_t * prefix_start;
get_param( data->buff,
data->buff_pos,
0,
&prefix_start,
0,
0 );
len = &data->buff[data->buff_pos]-prefix_start+1;
if( len <= PREFIX_MAX_LEN )
{
prefix = (wchar_t *)malloc( sizeof(wchar_t)*(len+1) );
wcslcpy( prefix, prefix_start, len );
prefix[len]=L'\0';
}
else
{
wchar_t tmp[2]=
{
ellipsis_char,
0
}
;
prefix = wcsdupcat( tmp,
prefix_start + (len - PREFIX_MAX_LEN) );
prefix[PREFIX_MAX_LEN] = 0;
}
{
int is_quoted;
wchar_t quote;
get_param( data->buff, data->buff_pos, &quote, 0, 0, 0 );
is_quoted = (quote != L'\0');
write_loop(1, "\n", 1 );
run_pager( prefix, is_quoted, comp );
}
free( prefix );
s_reset( &data->screen, 1 );
reader_repaint();
}
halloc_free( context );
return len;
}
/**
Initialize data for interactive use
*/
static void reader_interactive_init()
{
/* See if we are running interactively. */
pid_t shell_pgid;
input_init();
kill_init();
shell_pgid = getpgrp ();
/*
This should enable job control on fish, even if our parent process did
not enable it for us.
*/
/*
Check if we are in control of the terminal, so that we don't do
semi-expensive things like reset signal handlers unless we
really have to, which we often don't.
*/
if (tcgetpgrp( 0 ) != shell_pgid)
{
int block_count = 0;
int i;
/*
Bummer, we are not in control of the terminal. Stop until
parent has given us control of it. Stopping in fish is a bit
of a challange, what with all the signal fidgeting, we need
to reset a bunch of signal state, making this coda a but
unobvious.
In theory, reseting signal handlers could cause us to miss
signal deliveries. In practice, this code should only be run
suring startup, when we're not waiting for any signals.
*/
while (signal_is_blocked())
{
signal_unblock();
block_count++;
}
signal_reset_handlers();
/*
Ok, signal handlers are taken out of the picture. Stop ourself in a loop
until we are in control of the terminal.
*/
while (tcgetpgrp( 0 ) != shell_pgid)
{
killpg( shell_pgid, SIGTTIN);
}
signal_set_handlers();
for( i=0; i<block_count; i++ )
{
signal_block();
}
}
/* Put ourselves in our own process group. */
shell_pgid = getpid ();
if( getpgrp() != shell_pgid )
{
if (setpgid (shell_pgid, shell_pgid) < 0)
{
debug( 1,
_( L"Couldn't put the shell in its own process group" ));
wperror( L"setpgid" );
exit (1);
}
}
/* Grab control of the terminal. */
if( tcsetpgrp (STDIN_FILENO, shell_pgid) )
{
debug( 1,
_( L"Couldn't grab control of terminal" ) );
wperror( L"tcsetpgrp" );
exit(1);
}
common_handle_winch(0);
if( tcsetattr(0,TCSANOW,&shell_modes)) /* set the new modes */
{
wperror(L"tcsetattr");
}
/*
We need to know our own pid so we'll later know if we are a
fork
*/
original_pid = getpid();
env_set( L"_", L"fish", ENV_GLOBAL );
}
/**
Destroy data for interactive use
*/
static void reader_interactive_destroy()
{
kill_destroy();
writestr( L"\n" );
set_color( FISH_COLOR_RESET, FISH_COLOR_RESET );
input_destroy();
}
void reader_sanity_check()
{
if( is_interactive)
{
if( !data )
sanity_lose();
if(!( data->buff_pos <= data->buff_len ))
sanity_lose();
if(!( data->buff_len == wcslen( data->buff ) ))
sanity_lose();
}
}
void reader_replace_current_token( wchar_t *new_token )
{
wchar_t *begin, *end;
string_buffer_t sb;
int new_pos;
/*
Find current token
*/
parse_util_token_extent( data->buff, data->buff_pos, &begin, &end, 0, 0 );
if( !begin || !end )
return;
/*
Make new string
*/
sb_init( &sb );
sb_append_substring( &sb, data->buff, begin-data->buff );
sb_append( &sb, new_token );
sb_append( &sb, end );
new_pos = (begin-data->buff) + wcslen(new_token);
reader_set_buffer( (wchar_t *)sb.buff, new_pos );
sb_destroy( &sb );
}
/**
Set the specified string from the history as the current buffer. Do
not modify prefix_width.
*/
static void handle_history( const wchar_t *new_str )
{
if( new_str )
{
data->buff_len = wcslen( new_str );
check_size();
wcscpy( data->buff, new_str );
data->buff_pos=wcslen(data->buff);
reader_super_highlight_me_plenty( data->buff_pos, 0 );
reader_repaint();
}
}
/**
Check if the specified string is contained in the list, using
wcscmp as a comparison function
*/
static int contains_al( const wchar_t *needle,
array_list_t *haystack )
{
int i;
for( i=0; i<al_get_count( haystack ); i++ )
{
if( !wcscmp( needle, (const wchar_t *)al_get( haystack, i ) ) )
return 1;
}
return 0;
}
/**
Reset the data structures associated with the token search
*/
static void reset_token_history()
{
wchar_t *begin, *end;
parse_util_token_extent( data->buff, data->buff_pos, &begin, &end, 0, 0 );
sb_clear( &data->search_buff );
if( begin )
{
sb_append_substring( &data->search_buff, begin, end-begin);
}
data->token_history_pos = -1;
data->search_pos=0;
al_foreach( &data->search_prev, &free );
al_truncate( &data->search_prev, 0 );
al_push( &data->search_prev, wcsdup( (wchar_t *)data->search_buff.buff ) );
}
/**
Handles a token search command.
\param forward if the search should be forward or reverse
\param reset whether the current token should be made the new search token
*/
static void handle_token_history( int forward, int reset )
{
wchar_t *str=0;
int current_pos;
tokenizer tok;
if( reset )
{
/*
Start a new token search using the current token
*/
reset_token_history();
}
current_pos = data->token_history_pos;
if( forward || data->search_pos < (al_get_count( &data->search_prev )-1) )
{
if( forward )
{
if( data->search_pos > 0 )
{
data->search_pos--;
}
str = (wchar_t *)al_get( &data->search_prev, data->search_pos );
}
else
{
data->search_pos++;
str = (wchar_t *)al_get( &data->search_prev, data->search_pos );
}
reader_replace_current_token( str );
reader_super_highlight_me_plenty( data->buff_pos, 0 );
reader_repaint();
}
else
{
if( current_pos == -1 )
{
const wchar_t *item;
/*
Move to previous line
*/
free( (void *)data->token_history_buff );
/*
Search for previous item that contains this substring
*/
item = history_prev_match( (wchar_t *)data->search_buff.buff);
/*
If there is no match, the original string is returned
If so, we clear the match string to avoid infinite loop
*/
if( wcscmp( item, (wchar_t *)data->search_buff.buff ) == 0 )
{
item=L"";
}
data->token_history_buff = wcsdup( item );
current_pos = wcslen(data->token_history_buff);
}
if( ! wcslen( data->token_history_buff ) )
{
/*
We have reached the end of the history - check if the
history already contains the search string itself, if so
return, otherwise add it.
*/
const wchar_t *last = (const wchar_t *)al_get( &data->search_prev, al_get_count( &data->search_prev ) -1 );
if( wcscmp( last, (wchar_t *)data->search_buff.buff ) )
{
str = wcsdup( (wchar_t *)data->search_buff.buff );
}
else
{
return;
}
}
else
{
//debug( 3, L"new '%ls'", data->token_history_buff );
for( tok_init( &tok, data->token_history_buff, TOK_ACCEPT_UNFINISHED );
tok_has_next( &tok);
tok_next( &tok ))
{
switch( tok_last_type( &tok ) )
{
case TOK_STRING:
{
if( wcsstr( tok_last( &tok ), (wchar_t *)data->search_buff.buff ) )
{
//debug( 3, L"Found token at pos %d\n", tok_get_pos( &tok ) );
if( tok_get_pos( &tok ) >= current_pos )
{
break;
}
//debug( 3, L"ok pos" );
if( !contains_al( tok_last( &tok ), &data->search_prev ) )
{
free(str);
data->token_history_pos = tok_get_pos( &tok );
str = wcsdup(tok_last( &tok ));
}
}
}
}
}
tok_destroy( &tok );
}
if( str )
{
reader_replace_current_token( str );
reader_super_highlight_me_plenty( data->buff_pos, 0 );
reader_repaint();
al_push( &data->search_prev, str );
data->search_pos = al_get_count( &data->search_prev )-1;
}
else if( ! reader_interrupted() )
{
data->token_history_pos=-1;
handle_token_history( 0, 0 );
}
}
}
/**
Move buffer position one word or erase one word. This function
updates both the internal buffer and the screen. It is used by
M-left, M-right and ^W to do block movement or block erase.
\param dir Direction to move/erase. 0 means move left, 1 means move right.
\param erase Whether to erase the characters along the way or only move past them.
\param new if the new kill item should be appended to the previous kill item or not.
*/
static void move_word( int dir, int erase, int newv )
{
int end_buff_pos=data->buff_pos;
int step = dir?1:-1;
/*
Return if we are already at the edge
*/
if( !dir && data->buff_pos == 0 )
{
return;
}
if( dir && data->buff_pos == data->buff_len )
{
return;
}
/*
If we are beyond the last character and moving left, start by
moving one step, since otehrwise we'll start on the \0, which
should be ignored.
*/
if( !dir && (end_buff_pos == data->buff_len) )
{
if( !end_buff_pos )
return;
end_buff_pos--;
}
/*
When moving left, ignore the character under the cursor
*/
if( !dir )
{
end_buff_pos+=2*step;
}
/*
Remove all whitespace characters before finding a word
*/
while( 1 )
{
wchar_t c;
if( !dir )
{
if( end_buff_pos <= 0 )
break;
}
else
{
if( end_buff_pos >= data->buff_len )
break;
}
/*
Always eat at least one character
*/
if( end_buff_pos != data->buff_pos )
{
c = data->buff[end_buff_pos];
if( !iswspace( c ) )
{
break;
}
}
end_buff_pos+=step;
}
/*
Remove until we find a character that is not alphanumeric
*/
while( 1 )
{
wchar_t c;
if( !dir )
{
if( end_buff_pos <= 0 )
break;
}
else
{
if( end_buff_pos >= data->buff_len )
break;
}
c = data->buff[end_buff_pos];
if( !iswalnum( c ) )
{
/*
Don't gobble the boundary character when moving to the
right
*/
if( !dir )
end_buff_pos -= step;
break;
}
end_buff_pos+=step;
}
/*
Make sure we move at least one character
*/
if( end_buff_pos==data->buff_pos )
{
end_buff_pos+=step;
}
/*
Make sure we don't move beyond begining or end of buffer
*/
end_buff_pos = maxi( 0, mini( end_buff_pos, data->buff_len ) );
if( erase )
{
int remove_count = abs(data->buff_pos - end_buff_pos);
int first_char = mini( data->buff_pos, end_buff_pos );
// fwprintf( stderr, L"Remove from %d to %d\n", first_char, first_char+remove_count );
reader_kill( data->buff + first_char, remove_count, dir?KILL_APPEND:KILL_PREPEND, newv );
}
else
{
data->buff_pos = end_buff_pos;
reader_repaint();
}
}
wchar_t *reader_get_buffer(void)
{
ASSERT_IS_MAIN_THREAD();
return data?data->buff:NULL;
}
void reader_set_buffer( const wchar_t *b, int p )
{
int l = wcslen( b );
if( !data )
return;
data->buff_len = l;
check_size();
if( data->buff != b )
wcscpy( data->buff, b );
if( p>=0 )
{
data->buff_pos=mini( p, l );
}
else
{
data->buff_pos=l;
}
data->search_mode = NO_SEARCH;
sb_clear( &data->search_buff );
history_reset();
reader_super_highlight_me_plenty( data->buff_pos,
0 );
reader_repaint_needed();
}
int reader_get_cursor_pos()
{
if( !data )
return -1;
return data->buff_pos;
}
#define ENV_CMD_DURATION L"CMD_DURATION"
void set_env_cmd_duration(struct timeval *after, struct timeval *before)
{
time_t secs = after->tv_sec - before->tv_sec;
suseconds_t usecs = after->tv_usec - before->tv_usec;
wchar_t buf[16];
if (after->tv_usec < before->tv_usec) {
usecs += 1000000;
secs -= 1;
}
if (secs < 1) {
env_remove( ENV_CMD_DURATION, 0 );
} else {
if (secs < 10) { // 10 secs
swprintf(buf, 16, L"%lu.%02us", secs, usecs / 10000);
} else if (secs < 60) { // 1 min
swprintf(buf, 16, L"%lu.%01us", secs, usecs / 100000);
} else if (secs < 600) { // 10 mins
swprintf(buf, 16, L"%lum %lu.%01us", secs / 60, secs % 60, usecs / 100000);
} else if (secs < 5400) { // 1.5 hours
swprintf(buf, 16, L"%lum %lus", secs / 60, secs % 60);
} else {
swprintf(buf, 16, L"%.1fh", secs / 3600.0f);
}
env_set( ENV_CMD_DURATION, buf, ENV_EXPORT );
}
}
void reader_run_command( const wchar_t *cmd )
{
wchar_t *ft;
struct timeval time_before, time_after;
ft= tok_first( cmd );
if( ft != 0 )
env_set( L"_", ft, ENV_GLOBAL );
free(ft);
reader_write_title();
term_donate();
gettimeofday(&time_before, NULL);
eval( cmd, 0, TOP );
job_reap( 1 );
gettimeofday(&time_after, NULL);
set_env_cmd_duration(&time_after, &time_before);
term_steal();
env_set( L"_", program_name, ENV_GLOBAL );
#ifdef HAVE__PROC_SELF_STAT
proc_update_jiffies();
#endif
}
int reader_shell_test( wchar_t *b )
{
int res = parser_test( b, 0, 0, 0 );
if( res & PARSER_TEST_ERROR )
{
string_buffer_t sb;
sb_init( &sb );
int tmp[1];
int tmp2[1];
s_write( &data->screen, L"", L"", tmp, tmp2, 0 );
parser_test( b, 0, &sb, L"fish" );
fwprintf( stderr, L"%ls", sb.buff );
sb_destroy( &sb );
}
return res;
}
/**
Test if the given string contains error. Since this is the error
detection for general purpose, there are no invalid strings, so
this function always returns false.
*/
static int default_test( wchar_t *b )
{
return 0;
}
void reader_push( const wchar_t *name )
{
reader_data_t *n = (reader_data_t *)calloc( 1, sizeof( reader_data_t ) );
if( !n )
{
DIE_MEM();
}
n->name = wcsdup( name );
n->next = data;
sb_init( &n->kill_item );
data=n;
s_init( &data->screen );
sb_init( &data->prompt_buff );
check_size();
data->buff[0]=0;
sb_init( &data->search_buff );
if( data->next == 0 )
{
reader_interactive_init();
}
exec_prompt();
reader_set_highlight_function( &highlight_universal );
reader_set_test_function( &default_test );
reader_set_prompt( L"" );
history_set_mode( name );
al_init( &data->search_prev );
data->token_history_buff=0;
}
void reader_pop()
{
reader_data_t *n = data;
if( data == 0 )
{
debug( 0, _( L"Pop null reader block" ) );
sanity_lose();
return;
}
data=data->next;
free(n->name );
free( n->prompt );
free( n->buff );
free( n->color );
free( n->indent );
sb_destroy( &n->search_buff );
sb_destroy( &n->kill_item );
s_destroy( &n->screen );
sb_destroy( &n->prompt_buff );
/*
Clean up after history search
*/
al_foreach( &n->search_prev, &free );
al_destroy( &n->search_prev );
free( (void *)n->token_history_buff);
free(n);
if( data == 0 )
{
reader_interactive_destroy();
}
else
{
end_loop = 0;
history_set_mode( data->name );
s_reset( &data->screen, 1 );
}
}
void reader_set_prompt( const wchar_t *new_prompt )
{
free( data->prompt );
data->prompt=wcsdup(new_prompt);
}
void reader_set_complete_function( void (*f)( const wchar_t *,
array_list_t * ) )
{
data->complete_func = f;
}
void reader_set_highlight_function( highlight_function_t func )
{
data->highlight_function = func;
}
void reader_set_test_function( int (*f)( wchar_t * ) )
{
data->test_func = f;
}
/** A class as the context pointer for a background (threaded) highlight operation. */
class background_highlight_context {
public:
/**
The string to highlight
*/
wcstring buff;
/**
Malloc'd color buffer (same size as buff)
*/
int *color;
/**
The position to use for bracket matching
*/
int match_highlight_pos;
/**
Function for syntax highlighting
*/
highlight_function_t highlight_function;
/**
Environment variables
*/
env_vars vars;
/**
When the request was made
*/
double when;
background_highlight_context() : vars(env_vars::highlighting_keys)
{
}
~background_highlight_context()
{
free(color);
}
};
static void highlight_complete2( wchar_t *command, const int *colors, int position, void *ctx_ptr ) {
background_highlight_context *ctx = (background_highlight_context *)ctx_ptr;
if (ctx->buff == data->buff) {
/* The data hasn't changed, so swap in our colors */
free(data->color);
data->color = ctx->color;
ctx->color = NULL;
data->repaint_needed = 1;
}
/* Free our context */
delete ctx;
}
static void highlight_complete(void *ctx_ptr, int result) {
background_highlight_context *ctx = (background_highlight_context *)ctx_ptr;
if (ctx->buff == data->buff) {
/* The data hasn't changed, so swap in our colors */
free(data->color);
data->color = ctx->color;
ctx->color = NULL;
//data->repaint_needed = 1;
//s_reset( &data->screen, 1 );
reader_repaint();
}
/* Free our context */
delete ctx;
}
static int threaded_highlight(void *ctx_ptr) {
background_highlight_context *ctx = (background_highlight_context *)ctx_ptr;
array_list_t *error = 0;
const wchar_t *delayer = ctx->vars.get(L"HIGHLIGHT_DELAY");
double secDelay = 0;
if (delayer) {
wcstring tmp = delayer;
secDelay = from_string<double>(tmp);
}
if (secDelay > 0) usleep((useconds_t)(secDelay * 1E6));
//write(0, "Start", 5);
ctx->highlight_function( ctx->buff.c_str(), ctx->color, ctx->match_highlight_pos, error, ctx->vars );
//write(0, "End", 3);
return 0;
}
/**
Call specified external highlighting function and then do search
highlighting. Lastly, clear the background color under the cursor
to avoid repaint issues on terminals where e.g. syntax highligthing
maykes characters under the sursor unreadable.
\param match_highlight_pos the position to use for bracket matching. This need not be the same as the surrent cursor position
\param error if non-null, any possible errors in the buffer are further descibed by the strings inserted into the specified arraylist
*/
static void reader_super_highlight_me_plenty( int match_highlight_pos, array_list_t *error )
{
/* Ensure we don't repaint with stale data */
size_t i;
for (i=0; i < data->buff_sz; i++) {
data->color[i] = HIGHLIGHT_NORMAL;
}
background_highlight_context *ctx = new background_highlight_context;
ctx->buff = data->buff;
ctx->color = (int *)calloc(data->buff_sz, sizeof *ctx->color);
ctx->match_highlight_pos = match_highlight_pos;
ctx->highlight_function = data->highlight_function;
ctx->when = timef();
#if 1
iothread_perform(threaded_highlight, highlight_complete, ctx);
#else
data->highlight_function( ctx->buff, ctx->color, match_highlight_pos, error, highlight_complete2, ctx );
#endif
if( wcslen((wchar_t *)data->search_buff.buff) )
{
wchar_t * match = wcsstr( data->buff, (wchar_t *)data->search_buff.buff );
if( match )
{
int start = match-data->buff;
int count = wcslen( (wchar_t *)data->search_buff.buff );
int i;
for( i=0; i<count; i++ )
{
data->color[start+i] |= HIGHLIGHT_SEARCH_MATCH<<16;
}
}
}
}
int exit_status()
{
if( is_interactive )
return first_job == 0 && data->end_loop;
else
return end_loop;
}
/**
This function is called when the main loop notices that end_loop
has been set while in interactive mode. It checks if it is ok to
exit.
*/
static void handle_end_loop()
{
job_t *j;
int job_count=0;
int is_breakpoint=0;
block_t *b;
for( b = current_block;
b;
b = b->outer )
{
if( b->type == BREAKPOINT )
{
is_breakpoint = 1;
break;
}
}
for( j=first_job; j; j=j->next )
{
if( !job_is_completed(j) )
{
job_count++;
break;
}
}
if( !reader_exit_forced() && !data->prev_end_loop && job_count && !is_breakpoint )
{
writestr(_( L"There are stopped jobs. A second attempt to exit will enforce their termination.\n" ));
reader_exit( 0, 0 );
data->prev_end_loop=1;
}
else
{
if( !isatty(0) )
{
/*
We already know that stdin is a tty since we're
in interactive mode. If isatty returns false, it
means stdin must have been closed.
*/
for( j = first_job; j; j=j->next )
{
if( ! job_is_completed( j ) )
{
job_signal( j, SIGHUP );
}
}
}
}
}
/**
Read interactively. Read input from stdin while providing editing
facilities.
*/
static int read_i()
{
event_fire_generic(L"fish_prompt");
reader_push(L"fish");
reader_set_complete_function( &complete );
reader_set_highlight_function( &highlight_shell );
reader_set_test_function( &reader_shell_test );
data->prev_end_loop=0;
while( (!data->end_loop) && (!sanity_check()) )
{
wchar_t *tmp;
if( function_exists( PROMPT_FUNCTION_NAME ) )
reader_set_prompt( PROMPT_FUNCTION_NAME );
else
reader_set_prompt( DEFAULT_PROMPT );
/*
Put buff in temporary string and clear buff, so
that we can handle a call to reader_set_buffer
during evaluation.
*/
tmp = reader_readline();
if( data->end_loop)
{
handle_end_loop();
}
else if( tmp )
{
tmp = wcsdup( tmp );
data->buff_pos=data->buff_len=0;
data->buff[data->buff_len]=L'\0';
reader_run_command( tmp );
free( tmp );
if( data->end_loop)
{
handle_end_loop();
}
else
{
data->prev_end_loop=0;
}
}
}
reader_pop();
return 0;
}
/**
Test if there are bytes available for reading on the specified file
descriptor
*/
static int can_read( int fd )
{
struct timeval can_read_timeout = { 0, 0 };
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
return select(fd + 1, &fds, 0, 0, &can_read_timeout) == 1;
}
/**
Test if the specified character is in the private use area that
fish uses to store internal characters
*/
static int wchar_private( wchar_t c )
{
return ( (c >= 0xe000) && (c <= 0xf8ff ) );
}
/**
Test if the specified character in the specified string is
backslashed.
*/
static int is_backslashed( const wchar_t *str, int pos )
{
int count = 0;
int i;
for( i=pos-1; i>=0; i-- )
{
if( str[i] != L'\\' )
break;
count++;
}
return count %2;
}
wchar_t *reader_readline()
{
wint_t c;
int i;
int last_char=0, yank=0;
wchar_t *yank_str;
array_list_t *comp=0;
int comp_empty=1;
int finished=0;
struct termios old_modes;
check_size();
sb_clear( &data->search_buff );
data->buff[data->buff_len]='\0';
data->search_mode = NO_SEARCH;
exec_prompt();
reader_super_highlight_me_plenty( data->buff_pos, 0 );
s_reset( &data->screen, 1 );
reader_repaint();
/*
get the current terminal modes. These will be restored when the
function returns.
*/
tcgetattr(0,&old_modes);
/* set the new modes */
if( tcsetattr(0,TCSANOW,&shell_modes))
{
wperror(L"tcsetattr");
}
while( !finished && !data->end_loop)
{
int regular_char = 0;
/*
Sometimes strange input sequences seem to generate a zero
byte. I believe these simply mean a character was pressed
but it should be ignored. (Example: Trying to add a tilde
(~) to digit)
*/
while( 1 )
{
int was_interactive_read = is_interactive_read;
is_interactive_read = 1;
c=input_readch();
is_interactive_read = was_interactive_read;
if( ( (!wchar_private(c))) && (c>31) && (c != 127) )
{
if( can_read(0) )
{
wchar_t arr[READAHEAD_MAX+1];
int i;
memset( arr, 0, sizeof( arr ) );
arr[0] = c;
for( i=1; i<READAHEAD_MAX; i++ )
{
if( !can_read( 0 ) )
{
c = 0;
break;
}
c = input_readch();
if( (!wchar_private(c)) && (c>31) && (c != 127) )
{
arr[i]=c;
c=0;
}
else
break;
}
insert_str( arr );
}
}
if( c != 0 )
break;
}
/*
if( (last_char == R_COMPLETE) && (c != R_COMPLETE) && (!comp_empty) )
{
halloc_destroy( comp );
comp = 0;
}
*/
if( last_char != R_YANK && last_char != R_YANK_POP )
yank=0;
switch( c )
{
/* go to beginning of line*/
case R_BEGINNING_OF_LINE:
{
while( ( data->buff_pos>0 ) &&
( data->buff[data->buff_pos-1] != L'\n' ) )
{
data->buff_pos--;
}
reader_repaint();
break;
}
case R_END_OF_LINE:
{
while( data->buff[data->buff_pos] &&
data->buff[data->buff_pos] != L'\n' )
{
data->buff_pos++;
}
reader_repaint();
break;
}
case R_BEGINNING_OF_BUFFER:
{
data->buff_pos = 0;
reader_repaint();
break;
}
/* go to EOL*/
case R_END_OF_BUFFER:
{
data->buff_pos = data->buff_len;
reader_repaint();
break;
}
case R_NULL:
{
if( data->repaint_needed )
reader_repaint();
break;
}
case R_REPAINT:
{
exec_prompt();
write_loop( 1, "\r", 1 );
s_reset( &data->screen, 0 );
reader_repaint();
break;
}
case R_EOF:
{
exit_forced = 1;
data->end_loop=1;
break;
}
/* complete */
case R_COMPLETE:
{
if( !data->complete_func )
break;
if( comp_empty || last_char != R_COMPLETE)
{
wchar_t *begin, *end;
wchar_t *token_begin, *token_end;
wchar_t *buffcpy;
int len;
int cursor_steps;
parse_util_cmdsubst_extent( data->buff, data->buff_pos, &begin, &end );
parse_util_token_extent( begin, data->buff_pos - (begin-data->buff), &token_begin, &token_end, 0, 0 );
cursor_steps = token_end - data->buff- data->buff_pos;
data->buff_pos += cursor_steps;
if( is_backslashed( data->buff, data->buff_pos ) )
{
remove_backward();
}
reader_repaint();
len = data->buff_pos - (begin-data->buff);
buffcpy = wcsndup( begin, len );
comp = al_halloc( 0 );
data->complete_func( buffcpy, comp );
sort_completion_list( comp );
remove_duplicates( comp );
free( buffcpy );
comp_empty = handle_completions( comp );
halloc_free( comp );
comp = 0;
}
break;
}
/* kill */
case R_KILL_LINE:
{
wchar_t *begin = &data->buff[data->buff_pos];
wchar_t *end = begin;
int len;
while( *end && *end != L'\n' )
end++;
if( end==begin && *end )
end++;
len = end-begin;
if( len )
{
reader_kill( begin, len, KILL_APPEND, last_char!=R_KILL_LINE );
}
break;
}
case R_BACKWARD_KILL_LINE:
{
if( data->buff_pos > 0 )
{
wchar_t *end = &data->buff[data->buff_pos];
wchar_t *begin = end;
int len;
while( begin > data->buff && *begin != L'\n' )
begin--;
if( *begin == L'\n' )
begin++;
len = maxi( end-begin, 1 );
begin = end - len;
reader_kill( begin, len, KILL_PREPEND, last_char!=R_BACKWARD_KILL_LINE );
}
break;
}
case R_KILL_WHOLE_LINE:
{
wchar_t *end = &data->buff[data->buff_pos];
wchar_t *begin = end;
int len;
while( begin > data->buff && *begin != L'\n' )
begin--;
if( *begin == L'\n' )
begin++;
len = maxi( end-begin, 0 );
begin = end - len;
while( *end && *end != L'\n' )
end++;
if( begin == end && *end )
end++;
len = end-begin;
if( len )
{
reader_kill( begin, len, KILL_APPEND, last_char!=R_KILL_WHOLE_LINE );
}
break;
}
/* yank*/
case R_YANK:
{
yank_str = kill_yank();
insert_str( yank_str );
yank = wcslen( yank_str );
break;
}
/* rotate killring*/
case R_YANK_POP:
{
if( yank )
{
for( i=0; i<yank; i++ )
remove_backward();
yank_str = kill_yank_rotate();
insert_str(yank_str);
yank = wcslen(yank_str);
}
break;
}
/* Escape was pressed */
case L'\x1b':
{
if( data->search_mode )
{
data->search_mode= NO_SEARCH;
if( data->token_history_pos==-1 )
{
history_reset();
reader_set_buffer( (wchar_t *)data->search_buff.buff,
wcslen( (wchar_t *)data->search_buff.buff ) );
}
else
{
reader_replace_current_token( (wchar_t *)data->search_buff.buff );
}
sb_clear( &data->search_buff );
reader_super_highlight_me_plenty( data->buff_pos, 0 );
reader_repaint();
}
break;
}
/* delete backward*/
case R_BACKWARD_DELETE_CHAR:
{
remove_backward();
break;
}
/* delete forward*/
case R_DELETE_CHAR:
{
/**
Remove the current character in the character buffer and on the
screen using syntax highlighting, etc.
*/
if( data->buff_pos < data->buff_len )
{
data->buff_pos++;
remove_backward();
}
break;
}
/*
Evaluate. If the current command is unfinished, or if
the charater is escaped using a backslash, insert a
newline
*/
case R_EXECUTE:
{
/*
Allow backslash-escaped newlines
*/
if( is_backslashed( data->buff, data->buff_pos ) )
{
insert_char( '\n' );
break;
}
switch( data->test_func( data->buff ) )
{
case 0:
{
/*
Finished commend, execute it
*/
if( wcslen( data->buff ) )
{
// wcscpy(data->search_buff,L"");
history_add( data->buff );
}
finished=1;
data->buff_pos=data->buff_len;
reader_repaint();
break;
}
/*
We are incomplete, continue editing
*/
case PARSER_TEST_INCOMPLETE:
{
insert_char( '\n' );
break;
}
/*
Result must be some combination including an
error. The error message will already be
printed, all we need to do is repaint
*/
default:
{
s_reset( &data->screen, 1 );
reader_repaint();
break;
}
}
break;
}
/* History functions */
case R_HISTORY_SEARCH_BACKWARD:
case R_HISTORY_TOKEN_SEARCH_BACKWARD:
case R_HISTORY_SEARCH_FORWARD:
case R_HISTORY_TOKEN_SEARCH_FORWARD:
{
int reset = 0;
if( data->search_mode == NO_SEARCH )
{
reset = 1;
if( ( c == R_HISTORY_SEARCH_BACKWARD ) ||
( c == R_HISTORY_SEARCH_FORWARD ) )
{
data->search_mode = LINE_SEARCH;
}
else
{
data->search_mode = TOKEN_SEARCH;
}
sb_append( &data->search_buff, data->buff );
}
switch( data->search_mode )
{
case LINE_SEARCH:
{
const wchar_t *it = 0;
if( ( c == R_HISTORY_SEARCH_BACKWARD ) ||
( c == R_HISTORY_TOKEN_SEARCH_BACKWARD ) )
{
it = history_prev_match((wchar_t *)data->search_buff.buff);
}
else
{
it = history_next_match((wchar_t *)data->search_buff.buff);
}
handle_history( it );
break;
}
case TOKEN_SEARCH:
{
if( ( c == R_HISTORY_SEARCH_BACKWARD ) ||
( c == R_HISTORY_TOKEN_SEARCH_BACKWARD ) )
{
handle_token_history( SEARCH_BACKWARD, reset );
}
else
{
handle_token_history( SEARCH_FORWARD, reset );
}
break;
}
}
break;
}
/* Move left*/
case R_BACKWARD_CHAR:
{
if( data->buff_pos > 0 )
{
data->buff_pos--;
reader_repaint();
}
break;
}
/* Move right*/
case R_FORWARD_CHAR:
{
if( data->buff_pos < data->buff_len )
{
data->buff_pos++;
reader_repaint();
}
break;
}
/* kill one word left */
case R_BACKWARD_KILL_WORD:
{
move_word(0,1, last_char!=R_BACKWARD_KILL_WORD);
break;
}
/* kill one word right */
case R_KILL_WORD:
{
move_word(1,1, last_char!=R_KILL_WORD);
break;
}
/* move one word left*/
case R_BACKWARD_WORD:
{
move_word(0,0,0);
break;
}
/* move one word right*/
case R_FORWARD_WORD:
{
move_word( 1,0,0);
break;
}
case R_BEGINNING_OF_HISTORY:
{
history_first();
break;
}
case R_END_OF_HISTORY:
{
history_reset();
break;
}
case R_UP_LINE:
case R_DOWN_LINE:
{
int line_old = parse_util_get_line_from_offset( data->buff,
data->buff_pos );
int line_new;
if( c == R_UP_LINE )
line_new = line_old-1;
else
line_new = line_old+1;
int line_count = parse_util_lineno( data->buff, data->buff_len )-1;
if( line_new >= 0 && line_new <= line_count)
{
int base_pos_new;
int base_pos_old;
int indent_old;
int indent_new;
int line_offset_old;
int total_offset_new;
base_pos_new = parse_util_get_offset_from_line( data->buff,
line_new );
base_pos_old = parse_util_get_offset_from_line( data->buff,
line_old );
indent_old = data->indent[base_pos_old];
indent_new = data->indent[base_pos_new];
line_offset_old = data->buff_pos - parse_util_get_offset_from_line( data->buff,
line_old );
total_offset_new = parse_util_get_offset( data->buff, line_new, line_offset_old - 4*(indent_new-indent_old));
data->buff_pos = total_offset_new;
reader_repaint();
}
break;
}
/* Other, if a normal character, we add it to the command */
default:
{
if( (!wchar_private(c)) && (( (c>31) || (c==L'\n'))&& (c != 127)) )
{
regular_char = 1;
insert_char( c );
}
else
{
/*
Low priority debug message. These can happen if
the user presses an unefined control
sequnece. No reason to report.
*/
debug( 2, _( L"Unknown keybinding %d" ), c );
}
break;
}
}
if( (c != R_HISTORY_SEARCH_BACKWARD) &&
(c != R_HISTORY_SEARCH_FORWARD) &&
(c != R_HISTORY_TOKEN_SEARCH_BACKWARD) &&
(c != R_HISTORY_TOKEN_SEARCH_FORWARD) &&
(c != R_NULL) )
{
data->search_mode = NO_SEARCH;
sb_clear( &data->search_buff );
history_reset();
data->token_history_pos=-1;
}
last_char = c;
}
writestr( L"\n" );
/*
if( comp )
halloc_free( comp );
*/
if( !reader_exit_forced() )
{
if( tcsetattr(0,TCSANOW,&old_modes)) /* return to previous mode */
{
wperror(L"tcsetattr");
}
set_color( FISH_COLOR_RESET, FISH_COLOR_RESET );
}
return finished ? data->buff : 0;
}
int reader_search_mode()
{
if( !data )
{
return -1;
}
return !!data->search_mode;
}
/**
Read non-interactively. Read input from stdin without displaying
the prompt, using syntax highlighting. This is used for reading
scripts and init files.
*/
static int read_ni( int fd, io_data_t *io )
{
FILE *in_stream;
wchar_t *buff=0;
buffer_t acc;
int des = fd == 0 ? dup(0) : fd;
int res=0;
if (des == -1)
{
wperror( L"dup" );
return 1;
}
b_init( &acc );
in_stream = fdopen( des, "r" );
if( in_stream != 0 )
{
wchar_t *str;
int acc_used;
while(!feof( in_stream ))
{
char buff[4096];
int c;
c = fread(buff, 1, 4096, in_stream);
if( ferror( in_stream ) && ( errno != EINTR ) )
{
debug( 1,
_( L"Error while reading from file descriptor" ) );
/*
Reset buffer on error. We won't evaluate incomplete files.
*/
acc.used=0;
break;
}
b_append( &acc, buff, c );
}
b_append( &acc, "\0", 1 );
acc_used = acc.used;
str = str2wcs( acc.buff );
b_destroy( &acc );
if( fclose( in_stream ))
{
debug( 1,
_( L"Error while closing input stream" ) );
wperror( L"fclose" );
res = 1;
}
if( str )
{
string_buffer_t sb;
sb_init( &sb );
if( !parser_test( str, 0, &sb, L"fish" ) )
{
eval( str, io, TOP );
}
else
{
fwprintf( stderr, L"%ls", sb.buff );
res = 1;
}
sb_destroy( &sb );
free( str );
}
else
{
if( acc_used > 1 )
{
debug( 1,
_( L"Could not convert input. Read %d bytes." ),
acc_used-1 );
}
else
{
debug( 1,
_( L"Could not read input stream" ) );
}
res=1;
}
}
else
{
debug( 1,
_( L"Error while opening input stream" ) );
wperror( L"fdopen" );
free( buff );
res=1;
}
return res;
}
int reader_read( int fd, io_data_t *io )
{
int res;
/*
If reader_read is called recursively through the '.' builtin, we
need to preserve is_interactive. This, and signal handler setup
is handled by proc_push_interactive/proc_pop_interactive.
*/
int inter = ((fd == STDIN_FILENO) && isatty(STDIN_FILENO));
proc_push_interactive( inter );
res= is_interactive?read_i():read_ni( fd, io );
/*
If the exit command was called in a script, only exit the
script, not the program.
*/
if( data )
data->end_loop = 0;
end_loop = 0;
proc_pop_interactive();
return res;
}