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 "";
}
}