diff --git a/CMakeLists.txt b/CMakeLists.txt index 1de6862..5dc82c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,24 @@ if (NOT MINIMAL_FLAGS) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ggdb") endif (NOT MINIMAL_FLAGS) +find_program(BASHEXE bash /bin /usr/bin /usr/local/bin) +if(NOT DEFINED BASHEXE) + set(BASHEXE "/bin/bash") +endif(NOT DEFINED BASHEXE) configure_file(${CMAKE_SOURCE_DIR}/data/Startup/bash_startup.in ${CMAKE_BINARY_DIR}/Startup/bash_startup @ONLY) +configure_file(${CMAKE_SOURCE_DIR}/data/Startup/preexec.bash.in ${CMAKE_BINARY_DIR}/Startup/preexec.bash @ONLY) +configure_file(${CMAKE_SOURCE_DIR}/data/Termlets/bash/ps.in ${CMAKE_BINARY_DIR}/Termlets/bash/ps @ONLY) +configure_file(${CMAKE_SOURCE_DIR}/data/Termlets/bash/ls.in ${CMAKE_BINARY_DIR}/Termlets/bash/ls @ONLY) +configure_file(${CMAKE_SOURCE_DIR}/data/Termlets/bash/wget.in ${CMAKE_BINARY_DIR}/Termlets/bash/wget @ONLY) + +find_program(ZSHEXE zsh /bin /usr/bin /usr/local/bin) +if(NOT DEFINED ZSHEXE) + set(ZSHEXE "/usr/bin/zsh") +endif(NOT DEFINED ZSHEXE) +configure_file(${CMAKE_SOURCE_DIR}/data/Startup/zsh_startup.in ${CMAKE_BINARY_DIR}/Startup/zsh_startup @ONLY) +configure_file(${CMAKE_SOURCE_DIR}/data/Termlets/zsh/ps.in ${CMAKE_BINARY_DIR}/Termlets/zsh/ps @ONLY) +configure_file(${CMAKE_SOURCE_DIR}/data/Termlets/zsh/ls.in ${CMAKE_BINARY_DIR}/Termlets/zsh/ls @ONLY) +configure_file(${CMAKE_SOURCE_DIR}/data/Termlets/zsh/wget.in ${CMAKE_BINARY_DIR}/Termlets/zsh/wget @ONLY) set(PKGS clutter-gtk-1.0 mx-1.0 keybinder-3.0 gee-0.8) @@ -103,9 +120,9 @@ install(TARGETS finalterm RUNTIME DESTINATION bin) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/ColorSchemes DESTINATION share/finalterm) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/KeyBindings DESTINATION share/finalterm) install(DIRECTORY ${CMAKE_BINARY_DIR}/Startup DESTINATION share/finalterm) -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/Startup/preexec.bash DESTINATION share/finalterm/Startup) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/Startup/zsh_functions DESTINATION share/finalterm/Startup) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/TerminalCommands DESTINATION share/finalterm) -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/Termlets DESTINATION share/finalterm FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) +install(DIRECTORY ${CMAKE_BINARY_DIR}/Termlets DESTINATION share/finalterm FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/TextMenus DESTINATION share/finalterm) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/Themes DESTINATION share/finalterm) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/Icons/hicolor DESTINATION share/icons) diff --git a/README-ZSH.md b/README-ZSH.md new file mode 100644 index 0000000..f83be09 --- /dev/null +++ b/README-ZSH.md @@ -0,0 +1,8 @@ +Requirements +- $SHELL set to zsh +- add the following to ~/.zshrc: +``` +if [ -n "$FINALTERMSCRIPT" ]; then + . $FINALTERMSCRIPT +fi +``` diff --git a/data/Startup/bash_startup.in b/data/Startup/bash_startup.in index ff118eb..d8963fa 100644 --- a/data/Startup/bash_startup.in +++ b/data/Startup/bash_startup.in @@ -1,4 +1,4 @@ -#!/bin/bash +#!@BASHEXE@ # Include default startup file so that user's settings are respected [[ -r ~/.bashrc ]] && source ~/.bashrc @@ -112,14 +112,14 @@ export -f send_progress function run_termlet() { if [ -t 1 ]; then - "@PKGDATADIR@/Termlets/$@" + "@PKGDATADIR@/Termlets/bash/$@" else "$@" fi } # Set up termlet aliases -pushd "@PKGDATADIR@/Termlets" > /dev/null +pushd "@PKGDATADIR@/Termlets/bash" > /dev/null for filename in *; do alias $filename="run_termlet '$filename'" done diff --git a/data/Startup/preexec.bash b/data/Startup/preexec.bash.in similarity index 99% rename from data/Startup/preexec.bash rename to data/Startup/preexec.bash.in index 3057aee..3545ae9 100644 --- a/data/Startup/preexec.bash +++ b/data/Startup/preexec.bash.in @@ -1,4 +1,4 @@ -#!/bin/bash +#!@BASHEXE@ # NOTE: This file is taken (with minor modifications) from # Glyph Lefkowitz's blog post "This bash shell is now fully operational!" diff --git a/data/Startup/zsh_functions/final_term_control_sequence b/data/Startup/zsh_functions/final_term_control_sequence new file mode 100644 index 0000000..35ac81a --- /dev/null +++ b/data/Startup/zsh_functions/final_term_control_sequence @@ -0,0 +1,11 @@ +# NOTE: xterm properly ignores sequences of this type as unknown, +# while some other terminals (such as GNOME Terminal) print them +control_sequence="\e]133;" +for argument in "$@"; do + control_sequence="$control_sequence$argument;" +done +# TODO: Remove last semicolon +control_sequence="$control_sequence\a" + +# TODO: Should "-ne" be added here? +echo "$control_sequence" diff --git a/data/Startup/zsh_functions/send_control_sequence b/data/Startup/zsh_functions/send_control_sequence new file mode 100644 index 0000000..eac83bd --- /dev/null +++ b/data/Startup/zsh_functions/send_control_sequence @@ -0,0 +1,2 @@ +setopt no_prompt_cr +echo -ne "$1" diff --git a/data/Startup/zsh_functions/send_progress b/data/Startup/zsh_functions/send_progress new file mode 100644 index 0000000..e3e0411 --- /dev/null +++ b/data/Startup/zsh_functions/send_progress @@ -0,0 +1 @@ +send_control_sequence "$(final_term_control_sequence 'G' "$1" "$2")" diff --git a/data/Startup/zsh_functions/text_menu_end b/data/Startup/zsh_functions/text_menu_end new file mode 100644 index 0000000..061f4b1 --- /dev/null +++ b/data/Startup/zsh_functions/text_menu_end @@ -0,0 +1 @@ +echo "$(final_term_control_sequence 'F' "$1")" diff --git a/data/Startup/zsh_functions/text_menu_start b/data/Startup/zsh_functions/text_menu_start new file mode 100644 index 0000000..6eb63a3 --- /dev/null +++ b/data/Startup/zsh_functions/text_menu_start @@ -0,0 +1,3 @@ + # NOTE: Nested double quotes look strange, but are both valid and necessary; + # see http://stackoverflow.com/questions/4031007 + echo "$(final_term_control_sequence 'E' "$1")" diff --git a/data/Startup/zsh_startup.in b/data/Startup/zsh_startup.in new file mode 100644 index 0000000..5219d03 --- /dev/null +++ b/data/Startup/zsh_startup.in @@ -0,0 +1,82 @@ +#!@ZSHEXE@ + +# Final Term's customizations start here +finalterm_fpath="@PKGDATADIR@/Startup/zsh_functions" +if [ -d $finalterm_fpath ]; then + fpath=($finalterm_fpath $fpath) +fi +FPATH="${finalterm_fpath}:${FPATH}" +export FPATH + +autoload send_control_sequence +autoload final_term_control_sequence + + +# Logic for prompt and command detection +send_return_code() { + # Send sequence containing the return code of the last command + send_control_sequence "$(final_term_control_sequence 'D' "$?")" +} + +precmd_hook() { + # Send sequence marking a command prompt + send_control_sequence "$(final_term_control_sequence 'A')" +} +precmd_functions=( send_return_code $preexec_functions precmd_hook ) + +preexec_hook() { + # Send sequence containing the command to be executed + send_control_sequence "$(final_term_control_sequence 'C' "$1")" +} +preexec_functions=( $preexec_functions preexec_hook ) + +PROMPT="${PROMPT}$(final_term_control_sequence 'B')" + +# Logic for terminal commands +function trim() { + local text=$1 + text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters + text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters + echo -n "$text" +} + +function send_commands() { + send_control_sequence "$(final_term_control_sequence 'H' "$1" '#' "${@:2}")" +} + +pushd "@PKGDATADIR@/TerminalCommands" > /dev/null +while IFS= read -r line; do + stripped_line=$(trim "$line") + + if [ -n "$stripped_line" ]; then + # Non-empty line + if [ "${stripped_line:0:1}" != "#" ]; then + # Non-comment line + # Split on "=" character and escape double quotes used for command arguments + name=$(trim "${stripped_line%%\=*}") + cmds=$(trim "${${stripped_line#*\=}//\"/\\\"}") + + alias ",$name"="send_commands \"$cmds\"" + fi + fi +done <*.ftcommands +popd > /dev/null + + +# Termlet-related logic +function run_termlet() { + if [ -t 1 ]; then + "@PKGDATADIR@/Termlets/zsh/$@" + else + "$@" + fi +} + +# Set up termlet aliases +pushd "@PKGDATADIR@/Termlets/zsh" > /dev/null +for filename in *; do + alias $filename="run_termlet '$filename'" +done +popd > /dev/null + +cd ~ diff --git a/data/Termlets/ls b/data/Termlets/bash/ls.in similarity index 98% rename from data/Termlets/ls rename to data/Termlets/bash/ls.in index 5242d7a..a3731a8 100755 --- a/data/Termlets/ls +++ b/data/Termlets/bash/ls.in @@ -1,4 +1,4 @@ -#!/bin/bash +#!@BASHEXE@ ls_output=$(ls "$@") dir_begin_mark=$(text_menu_start '2') diff --git a/data/Termlets/ps b/data/Termlets/bash/ps.in similarity index 66% rename from data/Termlets/ps rename to data/Termlets/bash/ps.in index 8ef536d..f8bbb6f 100755 --- a/data/Termlets/ps +++ b/data/Termlets/bash/ps.in @@ -1,18 +1,25 @@ -#!/bin/bash +#!@BASHEXE@ # IFS is '\n' IFS=$'\012' psoutput=($(ps "$@")) + +headeridx=0 +while [ -z ${psoutput[$headeridx]+x} ]; do + ((headeridx+=1)) +done + # Just in case PPID could be displayed before PID, search for ' PID' -pid_index=$(awk -v a="${psoutput[0]}" -v b=' PID' 'BEGIN{print index(a,b)}') +pid_index=$(awk -v a="${psoutput[$headeridx]}" -v b=' PID' 'BEGIN{print index(a,b)}') # don’t use $() in loops, as it spawns a sub-process, so get markings out of the loop. begin_mark=$(text_menu_start '3') end_mark=$(text_menu_end '3') # last character position of PID display. let pid_end=pid_index+4 -# display 1st line as is, then destroy it. -echo -e ${psoutput[0]} -unset psoutput[0] + +# display header line as is and destroy it +echo -e ${psoutput[$headeridx]} +unset psoutput[$headeridx] for line in ${psoutput[@]}; do # Content line @@ -20,7 +27,7 @@ for line in ${psoutput[@]}; do pid_part=${left_part##* } # Remove pid_part from left_part left_part=${left_part:0:${#left_part}-${#pid_part}} - right_part=${line:pid_end-1} + right_part=${line:${pid_end}-1} modified_line="$left_part$begin_mark$pid_part$end_mark$right_part" echo -e "$modified_line" done diff --git a/data/Termlets/wget b/data/Termlets/bash/wget.in similarity index 98% rename from data/Termlets/wget rename to data/Termlets/bash/wget.in index e424376..14fd27b 100755 --- a/data/Termlets/wget +++ b/data/Termlets/bash/wget.in @@ -1,4 +1,4 @@ -#!/bin/bash +#!@BASHEXE@ # TODO: Multiple file downloads? diff --git a/data/Termlets/zsh/ls.in b/data/Termlets/zsh/ls.in new file mode 100755 index 0000000..68c91b8 --- /dev/null +++ b/data/Termlets/zsh/ls.in @@ -0,0 +1,38 @@ +#!@ZSHEXE@ + +autoload text_menu_start +autoload text_menu_end +autoload final_term_control_sequence +autoload send_control_sequence + +ls_output=$(ls "$@") +dir_begin_mark=$(text_menu_start '2') +dir_end_mark=$(text_menu_end '2') +file_begin_mark=$(text_menu_start '1') +file_end_mark=$(text_menu_end '1') + +# Surround with additional newlines to facilitate matching (see below) +ls_output=$'\n'$ls_output$'\n' + +# TODO: Search for files in directory passed to ls rather than the current directory +for filename in *; do + if [[ -d $filename ]]; then + file_substitution="$dir_begin_mark$filename$dir_end_mark" + else + file_substitution="$file_begin_mark$filename$file_end_mark" + fi + + # Short format ("ls"; each filename on a single line) + ls_output=${ls_output/$'\n'$filename$'\n'/$'\n'$file_substitution$'\n'} + # Long format ("ls -l") + ls_output=${ls_output/ $filename$'\n'/ $file_substitution$'\n'} + # Long format; symlinks + ls_output=${ls_output/ $filename ->/ $file_substitution ->} +done + +# Strip leading newline +ls_output=${ls_output#$'\n'} +# Strip trailing newline +ls_output=${ls_output%$'\n'} + +echo -e "$ls_output" diff --git a/data/Termlets/zsh/ps.in b/data/Termlets/zsh/ps.in new file mode 100755 index 0000000..6b19441 --- /dev/null +++ b/data/Termlets/zsh/ps.in @@ -0,0 +1,38 @@ +#!@ZSHEXE@ + +autoload text_menu_start +autoload text_menu_end +autoload final_term_control_sequence +autoload send_control_sequence + +# IFS is '\n' +IFS=$'\012' +psoutput=($(ps "$@")) + +headeridx=0 +while [ -z ${psoutput[$headeridx]+x} ]; do + ((headeridx+=1)) +done + +# Just in case PPID could be displayed before PID, search for ' PID' +pid_index=$(awk -v a="${psoutput[$headeridx]}" -v b=' PID' 'BEGIN{print index(a,b)}') +# don’t use $() in loops, as it spawns a sub-process, so get markings out of the loop. +begin_mark=$(text_menu_start '3') +end_mark=$(text_menu_end '3') +# last character position of PID display. +let pid_end=pid_index+4 + +# display header line as is and destroy it +echo -e ${psoutput[$headeridx]} +psoutput[$headeridx]=() + +for line in ${psoutput[@]}; do + # Content line + left_part=${line:0:${pid_end}-1} + pid_part=${left_part##* } + # Remove pid_part from left_part + left_part=${left_part:0:${#left_part}-${#pid_part}} + right_part=${line:${pid_end}-1} + modified_line="$left_part$begin_mark$pid_part$end_mark$right_part" + echo -e "$modified_line" +done diff --git a/data/Termlets/zsh/wget.in b/data/Termlets/zsh/wget.in new file mode 100755 index 0000000..f64988d --- /dev/null +++ b/data/Termlets/zsh/wget.in @@ -0,0 +1,35 @@ +#!@ZSHEXE@ + +autoload send_progress +autoload send_control_sequence +autoload final_term_control_sequence +setopt BASH_REMATCH + +# TODO: Multiple file downloads? + +# Note that wget writes its output to STDERR instead of STDOUT +wget --progress=bar:force "$@" 2>&1 | + +while IFS= read -r line; do + echo "$line" + + if [[ $line == "" ]]; then + # Progress bar reached + # => Switch to CR as line separator to receive + # individual progress bar updates + while IFS= read -r -d $'\r' line; do + # Extract current progress percentage + if [[ $line =~ ^\ ?([0-9]{1,3})% ]]; then + send_progress "${BASH_REMATCH[1]}" "Downloading $1..." + fi + + echo -ne "\r$line" + done + + # Process completed + send_progress "-1" "" + + # Print remaining output + echo -ne "\r$line" + fi +done diff --git a/data/org.gnome.finalterm.gschema.xml b/data/org.gnome.finalterm.gschema.xml index 63839cc..5de5e3d 100644 --- a/data/org.gnome.finalterm.gschema.xml +++ b/data/org.gnome.finalterm.gschema.xml @@ -46,7 +46,7 @@ '/bin/bash' - Path to the shell executable which is to be run (NOTE: Only bash is currently supported) + Path to the shell executable which is to be run (NOTE: Only bash and zsh are currently supported) diff --git a/src/Command.vala b/src/Command.vala index 2fcf14f..123a40b 100644 --- a/src/Command.vala +++ b/src/Command.vala @@ -97,17 +97,22 @@ public class Command : Object { foreach (var parameter in parameters) { var substitute_parameter = parameter; - // Replace placeholder "%i" with placeholder_substitutes[i - 1] - for (int i = 0; i < placeholder_substitutes.size; i++) { - substitute_parameter = substitute_parameter.replace( - "%" + (i + 1).to_string(), - placeholder_substitutes.get(i)); + try { + // Replace placeholder "%i" with placeholder_substitutes[i - 1] + for (int i = 0; i < placeholder_substitutes.size; i++) { + substitute_parameter = substitute_parameter.replace( + "%" + (i + 1).to_string(), + placeholder_substitutes.get(i)); + message(_("placeholder_substitute: %s"), placeholder_substitutes.get(i)); + } + + // Remove remaining placeholders + substitute_parameter = placeholder_pattern.replace(substitute_parameter, + -1, 0, ""); + substitute_command.parameters.add(substitute_parameter); + } catch (GLib.RegexError e) { + error(_("Error substituting parameter. placeholder_pattern: %s substitute_parameter: %s, exception: %s"), placeholder_pattern.get_pattern, substitute_parameter, e.message); } - - // Remove remaining placeholders - substitute_parameter = placeholder_pattern.replace(substitute_parameter, -1, 0, ""); - - substitute_command.parameters.add(substitute_parameter); } substitute_command.execute(); diff --git a/src/Terminal.vala b/src/Terminal.vala index c9e1eb5..5b043a0 100644 --- a/src/Terminal.vala +++ b/src/Terminal.vala @@ -242,8 +242,29 @@ public class Terminal : Object { private void run_shell() { Environment.set_variable("TERM", Settings.get_default().emulated_terminal, true); - string[] arguments = { Settings.get_default().shell_path, "--rcfile", - Config.PKGDATADIR + "/Startup/bash_startup", "-i" }; + string shell = Environment.get_variable("SHELL") ?? Settings.get_default().shell_path; + string shell_basename = Filename.display_basename(shell); + + string[] valid_shells = { "zsh", "bash" }; + + if (!(shell_basename in valid_shells)){ + message(_("shell defined in environment is not supported, falling back to bash")); + shell = Settings.get_default().shell_path; + shell_basename = Filename.display_basename(shell); + } + + string shell_include = Config.PKGDATADIR + "/Startup/" + shell_basename + "_startup"; + + string[] arguments = {}; + switch(shell_basename){ + case "bash": + arguments = { shell, "--rcfile", shell_include, "-i" }; + break; + case "zsh": + Environment.set_variable("FINALTERMSCRIPT", shell_include, true); + arguments = { shell, "-i" }; + break; + } // Add custom shell arguments foreach (var argument in Settings.get_default().shell_arguments) { @@ -251,7 +272,7 @@ public class Terminal : Object { } // Replace child process with shell process - Posix.execvp(Settings.get_default().shell_path, arguments); + Posix.execvp(shell, arguments); // If this line is reached, execvp() must have failed critical(_("execvp failed")); diff --git a/src/TerminalOutput.vala b/src/TerminalOutput.vala index 93cde65..3d67938 100644 --- a/src/TerminalOutput.vala +++ b/src/TerminalOutput.vala @@ -383,7 +383,7 @@ public class TerminalOutput : Gee.ArrayList { warning(_("Command start control sequence received while already in command mode")); command_mode = true; command_start_position = cursor_position; - message(_("Command mode entered")); + message(_("Command mode entered, cursor_position (line: %i column: %i)"), cursor_position.line, cursor_position.column); break; case TerminalStream.StreamElement.ControlSequenceType.FTCS_COMMAND_EXECUTED: @@ -550,8 +550,12 @@ public class TerminalOutput : Gee.ArrayList { public string get_command() { // TODO: Revisit this check (condition should never fail) if (command_start_position.compare(cursor_position) < 0) { + warning(_("command_start_position: (%i,%i) cursor_postion: (%i,%i)"), + command_start_position.line, command_start_position.column, + cursor_position.line, cursor_position.column); return get_range(command_start_position, cursor_position); } else { + warning(_("cursor_position < command_start_position")); return ""; } }