diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ad3119f..cddacfcdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - A new variable, `$fish_vi_force_cursor`, has been added. This can be set to force `fish_vi_cursor` to attempt changing the cursor shape in vi mode, regardless of terminal. Additionally, the `fish_vi_cursor` option `--force-iterm` has been deprecated; all usages can be replaced by setting `$fish_vi_force_cursor`. - The history file is now created with user-private permissions, matching other shells (#6926). The directory containing the history file remains private, so there should not have been any private date revealed. - fish no longer disables flow control after every command. Enterprising users can now enable it for external commands with `stty`. (#2315) +- Added a `fish_job_summary` function which is called whenever a background job stops or ends, or any job terminates from a signal. The default behaviour can now be customized by redefining this function. ### Syntax changes and new commands diff --git a/share/functions/fish_job_summary.fish b/share/functions/fish_job_summary.fish new file mode 100644 index 000000000..a27ca89b1 --- /dev/null +++ b/share/functions/fish_job_summary.fish @@ -0,0 +1,38 @@ +function fish_job_summary -a job_id cmd_line signal_or_end_name signal_desc proc_pid proc_name + # job_id: ID of the job that stopped/terminated/ended. + # cmd_line: The command line of the job. + # signal_or_end_name: If terminated by signal, the name of the signal (e.g. SIGTERM). + # If ended, the string "ENDED". If stopped, the string "STOPPED". + # signal_desc: A description of the signal (e.g. "Polite quite request"). + # Not provided if the job stopped or ended without a signal. + # If the job has more than one process: + # proc_pid: the pid of the process affected. + # proc_name: the name of that process. + # If the job has only one process, these two arguments will not be provided. + + set -l ellipsis '...' + if string match -iqr 'utf.?8' -- $LANG + set ellipsis \u2026 + end + + set -l max_cmd_len 32 + if test (string length $cmd_line) -gt $max_cmd_len + set -l truncated_len (math $max_cmd_len - (string length $ellipsis)) + set cmd_line (string trim (string sub -l $truncated_len $cmd_line))$ellipsis + end + + switch $signal_or_end_name + case "STOPPED" + printf ( _ "fish: Job %s, '%s' has stopped\n" ) $job_id $cmd_line + case "ENDED" + printf ( _ "fish: Job %s, '%s' has ended\n" ) $job_id $cmd_line + case 'SIG*' + if test -n "$proc_pid" + printf ( _ "fish: Process %s, '%s' from job %s, '%s' terminated by signal %s (%s)\n" ) \ + $proc_pid $proc_name $job_id $cmd_line $signal_or_end_name $signal_desc + else + printf ( _ "fish: Job %s, '%s' terminated by signal %s (%s)\n" ) \ + $job_id $cmd_line $signal_or_end_name $signal_desc + end + end >&2 +end diff --git a/src/proc.cpp b/src/proc.cpp index f839695b8..7b4c00caa 100644 --- a/src/proc.cpp +++ b/src/proc.cpp @@ -453,40 +453,34 @@ static void process_mark_finished_children(parser_t &parser, bool block_ok) { reap_disowned_pids(); } -/// Given a command like "cat file", truncate it to a reasonable length. -static wcstring truncate_command(const wcstring &cmd) { - const size_t max_len = 32; - if (cmd.size() <= max_len) { - // No truncation necessary. - return cmd; +/// Call the fish_job_summary function with the given args. +static void print_job_summary(parser_t &parser, const wcstring_list_t &args) { + wcstring buffer = wcstring(L"fish_job_summary"); + for (const wcstring &arg : args) { + buffer.push_back(L' '); + buffer.append(escape_string(arg, ESCAPE_ALL)); } + event_t event(event_type_t::generic); + event.desc.str_param1 = L"fish_job_summary"; - // Truncation required. - const wchar_t *ellipsis_str = get_ellipsis_str(); - const size_t ellipsis_length = std::wcslen(ellipsis_str); // no need for wcwidth - size_t trunc_length = max_len - ellipsis_length; - // Eat trailing whitespace. - while (trunc_length > 0 && iswspace(cmd.at(trunc_length - 1))) { - trunc_length -= 1; - } - wcstring result = wcstring(cmd, 0, trunc_length); - // Append ellipsis. - result.append(ellipsis_str); - return result; + auto prev_statuses = parser.get_last_statuses(); + + block_t *b = parser.push_block(block_t::event_block(event)); + parser.eval(buffer, io_chain_t()); + parser.pop_block(b); + + parser.set_last_statuses(std::move(prev_statuses)); } /// Format information about job status for the user to look at. using job_status_t = enum { JOB_STOPPED, JOB_ENDED }; -static void print_job_status(const job_t *j, job_status_t status) { - const wchar_t *msg = L"Job %d, '%ls' has ended"; // this is the most common status msg - if (status == JOB_STOPPED) msg = L"Job %d, '%ls' has stopped"; - outputter_t outp; - outp.writestr("\r"); - outp.writestr(format_string(_(msg), j->job_id(), truncate_command(j->command()).c_str())); - if (clr_eol) outp.term_puts(clr_eol, 1); - outp.writestr(L"\n"); - fflush(stdout); - outp.flush_to(STDOUT_FILENO); +static void print_job_status(parser_t &parser, const job_t *j, job_status_t status) { + wcstring_list_t args = { + format_string(L"%d", j->job_id()), + j->command(), + status == JOB_STOPPED ? L"STOPPED" : L"ENDED", + }; + print_job_summary(parser, args); } event_t proc_create_event(const wchar_t *msg, event_type_t type, pid_t pid, int status) { @@ -517,8 +511,8 @@ void remove_disowned_jobs(job_list_t &jobs) { /// Given a a process in a job, print the status message for the process as appropriate, and then /// mark the status code so we don't print again. Populate any events into \p exit_events. /// \return true if we printed a status message, false if not. -static bool try_clean_process_in_job(process_t *p, job_t *j, std::vector *exit_events, - bool only_one_job) { +static bool try_clean_process_in_job(parser_t &parser, process_t *p, job_t *j, + std::vector *exit_events) { if (!p->completed || !p->pid) { return false; } @@ -556,27 +550,17 @@ static bool try_clean_process_in_job(process_t *p, job_t *j, std::vectoris_foreground()) { - if (proc_is_job) { - // We want to report the job number, unless it's the only job, in which case - // we don't need to. - const wcstring job_number_desc = - only_one_job ? wcstring() : format_string(_(L"Job %d, "), j->job_id()); - std::fwprintf(stdout, _(L"%ls: %ls\'%ls\' terminated by signal %ls (%ls)"), - program_name, job_number_desc.c_str(), - truncate_command(j->command()).c_str(), sig2wcs(s.signal_code()), - signal_get_desc(s.signal_code())); - } else { - const wcstring job_number_desc = - only_one_job ? wcstring() : format_string(L"from job %d, ", j->job_id()); - const wchar_t *fmt = - _(L"%ls: Process %d, \'%ls\' %ls\'%ls\' terminated by signal %ls (%ls)"); - std::fwprintf(stdout, fmt, program_name, p->pid, p->argv0(), job_number_desc.c_str(), - truncate_command(j->command()).c_str(), sig2wcs(s.signal_code()), - signal_get_desc(s.signal_code())); + wcstring_list_t args; + args.reserve(proc_is_job ? 4 : 6); + args.push_back(format_string(L"%d", j->job_id())); + args.push_back(j->command()); + args.push_back(sig2wcs(s.signal_code())); + args.push_back(signal_get_desc(s.signal_code())); + if (!proc_is_job) { + args.push_back(format_string(L"%d", p->pid)); + args.push_back(p->argv0()); } - - if (clr_eol) outputter_t::stdoutput().term_puts(clr_eol, 1); - std::fwprintf(stdout, L"\n"); + print_job_summary(parser, args); printed = true; } // Clear status so it is not reported more than once. @@ -635,7 +619,6 @@ static bool process_clean_after_marking(parser_t &parser, bool allow_interactive }; // Print status messages for completed or stopped jobs. - const bool only_one_job = parser.jobs().size() == 1; for (const auto &j : parser.jobs()) { if (!should_process_job(j)) continue; @@ -643,14 +626,14 @@ static bool process_clean_after_marking(parser_t &parser, bool allow_interactive // Note this may print the message on behalf of the job, affecting the result of // job_wants_message(). for (process_ptr_t &p : j->processes) { - if (try_clean_process_in_job(p.get(), j.get(), &exit_events, only_one_job)) { + if (try_clean_process_in_job(parser, p.get(), j.get(), &exit_events)) { printed = true; } } // Print the message if we need to. if (job_wants_message(j) && (j->is_completed() || j->is_stopped())) { - print_job_status(j.get(), j->is_completed() ? JOB_ENDED : JOB_STOPPED); + print_job_status(parser, j.get(), j->is_completed() ? JOB_ENDED : JOB_STOPPED); j->mut_flags().notified = true; printed = true; } diff --git a/tests/job_summary.expect b/tests/job_summary.expect new file mode 100644 index 000000000..296bea8b8 --- /dev/null +++ b/tests/job_summary.expect @@ -0,0 +1,39 @@ +# vim: set filetype=expect: +# +# Test job summary for interactive shells. + +set pid [spawn $fish] +expect_prompt + +send_line "function fish_job_summary; string join ':' \$argv; end" +expect_prompt + +# fish_job_summary is called when background job ends. +send_line "sleep 0.5 &" +sleep 0.050 +expect_prompt +sleep 0.550 +expect -re "\[0-9]+:sleep 0.5 &:ENDED" +send_line "" +expect_prompt + +# fish_job_summary is called when background job is signalled. +# cmd_line correctly prints only the actually backgrounded job. +send_line "false; sleep 10 &; true" +sleep 0.100 +expect_prompt +exec -- pkill -TERM sleep -P $pid +sleep 0.100 +expect -re "\[0-9]+:sleep 10 &:SIGTERM:Polite quit request" +send_line "" +expect_prompt + +# fish_job_summary is called when foreground job is signalled. +# cmd_line contains the entire pipeline. proc_id and proc_name are set in a pipeline. +send_line "true | sleep 6" +sleep 0.100 +exec -- pkill -KILL sleep -P $pid +sleep 0.100 +expect -re "\[0-9]+:true | sleep 6:SIGKILL:Forced quit:\[0-9]+:sleep" +send_line "" +expect_prompt diff --git a/tests/job_summary.expect.err b/tests/job_summary.expect.err new file mode 100644 index 000000000..e69de29bb diff --git a/tests/job_summary.expect.out b/tests/job_summary.expect.out new file mode 100644 index 000000000..e69de29bb