mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 16:37:27 +00:00
notify_on_cmd_finish: Show the actual command that was finished
Fixes #7420
This commit is contained in:
parent
c50e38a080
commit
0d68a21be5
9 changed files with 123 additions and 56 deletions
|
|
@ -53,6 +53,10 @@ Detailed list of changes
|
|||
0.35.0 [future]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- :opt:`notify_on_cmd_finish`: Show the actual command that was finished (:iss:`7420`)
|
||||
|
||||
- Shell integration: Make the currently executing cmdline available as a window variable in kitty
|
||||
|
||||
- :opt:`paste_actions`: Fix ``replace-newline`` not working with ``confirm`` (:iss:`7374`)
|
||||
|
||||
- Graphics: Fix aspect ratio of images not being preserved when only a single
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ functions for the events you are interested in, for example:
|
|||
|
||||
def on_cmd_startstop(boss: Boss, window: Window, data: Dict[str, Any]) -> None:
|
||||
# called when the shell starts/stops executing a command. Here
|
||||
# data will contain is_start and time.
|
||||
# data will contain is_start, cmdline and time.
|
||||
|
||||
Every callback is passed a reference to the global ``Boss`` object as well as
|
||||
the ``Window`` object the action is occurring on. The ``data`` object is a dict
|
||||
|
|
|
|||
|
|
@ -433,6 +433,10 @@ Just before running a command/program, send the escape code::
|
|||
|
||||
<OSC>133;C<ST>
|
||||
|
||||
Optionally, when a command is finished its "exit status" can be reported as::
|
||||
|
||||
<OSC>133;D;exit status as base 10 integer<ST>
|
||||
|
||||
Here ``<OSC>`` is the bytes ``0x1b 0x5d`` and ``<ST>`` is the bytes ``0x1b
|
||||
0x5c``. This is exactly what is needed for shell integration in kitty. For the
|
||||
full protocol, that also marks the command region, see `the iTerm2 docs
|
||||
|
|
@ -451,3 +455,13 @@ to control its behavior, separated by semi-colons. They are::
|
|||
|
||||
k=s - this tells kitty that the secondary (PS2) prompt is starting at the
|
||||
current line.
|
||||
|
||||
kitty also optionally supports sending the cmdline going to be executed with ``<OSC>133;C`` as::
|
||||
|
||||
<OSC>133;C;cmdline=cmdline as space separated hex encoded text<ST>
|
||||
or
|
||||
<OSC>133;C;cmdline_url=cmdline as UTF-8 URL escaped text<ST>
|
||||
|
||||
|
||||
Here, *space separated hex encoded text* means every unicode codepoint of the
|
||||
command line is encoded as 2-8 hex digits separated by spaces.
|
||||
|
|
|
|||
|
|
@ -3216,7 +3216,8 @@ Some more examples::
|
|||
# Ring a bell when a command takes more than 10 seconds in a invisible window
|
||||
notify_on_cmd_finish invisible 10.0 bell
|
||||
# Run 'notify-send' when a command takes more than 10 seconds in a invisible window
|
||||
notify_on_cmd_finish invisible 10.0 command notify-send job finished
|
||||
# Here %c is replaced by the current command line and %s by the job exit code
|
||||
notify_on_cmd_finish invisible 10.0 command notify-send "job finished with status: %s" %c
|
||||
'''
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2335,13 +2335,20 @@ shell_prompt_marking(Screen *self, char *buf) {
|
|||
self->prompt_settings.uses_special_keys_for_cursor_movement = 0;
|
||||
parse_prompt_mark(self, buf+1, &pk);
|
||||
self->linebuf->line_attrs[self->cursor->y].prompt_kind = pk;
|
||||
if (pk == PROMPT_START)
|
||||
CALLBACK("cmd_output_marking", "O", Py_False);
|
||||
if (pk == PROMPT_START) CALLBACK("cmd_output_marking", "O", Py_False);
|
||||
} break;
|
||||
case 'C':
|
||||
case 'C': {
|
||||
self->linebuf->line_attrs[self->cursor->y].prompt_kind = OUTPUT_START;
|
||||
CALLBACK("cmd_output_marking", "O", Py_True);
|
||||
break;
|
||||
const char *cmdline = "";
|
||||
if (strstr(buf + 1, ";cmdline") == buf + 1) {
|
||||
cmdline = buf + 2;
|
||||
}
|
||||
CALLBACK("cmd_output_marking", "Os", Py_True, cmdline);
|
||||
} break;
|
||||
case 'D': {
|
||||
const char *exit_status = buf[1] == ';' ? buf + 2 : "";
|
||||
CALLBACK("cmd_output_marking", "Os", Py_None, exit_status);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,6 +217,16 @@ def compile_match_query(exp: str, is_simple: bool = True) -> MatchPatternType:
|
|||
return pat
|
||||
|
||||
|
||||
def decode_cmdline(x: str) -> str:
|
||||
ctype, sep, val = x.partition('=')
|
||||
if ctype == 'cmdline':
|
||||
return ''.join(chr(int(x, 16)) for x in val.split())
|
||||
if ctype == 'cmdline_url':
|
||||
from urllib.parse import unquote
|
||||
return unquote(val)
|
||||
return ''
|
||||
|
||||
|
||||
class WindowDict(TypedDict):
|
||||
id: int
|
||||
is_focused: bool
|
||||
|
|
@ -225,6 +235,8 @@ class WindowDict(TypedDict):
|
|||
pid: Optional[int]
|
||||
cwd: str
|
||||
cmdline: List[str]
|
||||
last_reported_cmdline: str
|
||||
last_cmd_exit_status: int
|
||||
env: Dict[str, str]
|
||||
foreground_processes: List[ProcessDesc]
|
||||
is_self: bool
|
||||
|
|
@ -550,6 +562,8 @@ class Window:
|
|||
self.current_clipboard_read_ask: Optional[bool] = None
|
||||
self.prev_osc99_cmd = NotificationCommand()
|
||||
self.last_cmd_output_start_time = 0.
|
||||
self.last_cmd_cmdline = ''
|
||||
self.last_cmd_exit_status = 0
|
||||
self.actions_on_close: List[Callable[['Window'], None]] = []
|
||||
self.actions_on_focus_change: List[Callable[['Window', bool], None]] = []
|
||||
self.actions_on_removal: List[Callable[['Window'], None]] = []
|
||||
|
|
@ -680,6 +694,8 @@ class Window:
|
|||
'pid': self.child.pid,
|
||||
'cwd': self.child.current_cwd or self.child.cwd,
|
||||
'cmdline': self.child.cmdline,
|
||||
'last_reported_cmdline': self.last_cmd_cmdline,
|
||||
'last_cmd_exit_status': self.last_cmd_exit_status,
|
||||
'env': self.child.environ or self.child.final_env,
|
||||
'foreground_processes': self.child.foreground_processes,
|
||||
'is_self': is_self,
|
||||
|
|
@ -703,6 +719,8 @@ class Window:
|
|||
'cwd': self.child.current_cwd or self.child.cwd,
|
||||
'env': self.child.environ,
|
||||
'cmdline': self.child.cmdline,
|
||||
'last_reported_cmdline': self.last_cmd_cmdline,
|
||||
'last_cmd_exit_status': self.last_cmd_exit_status,
|
||||
'margin': self.margin.serialize(),
|
||||
'user_vars': self.user_vars,
|
||||
'padding': self.padding.serialize(),
|
||||
|
|
@ -1374,41 +1392,52 @@ class Window:
|
|||
if self.child_title:
|
||||
self.title_stack.append(self.child_title)
|
||||
|
||||
def cmd_output_marking(self, is_start: bool) -> None:
|
||||
def handle_cmd_end(self, exit_status: str = '') -> None:
|
||||
if self.last_cmd_output_start_time == 0.:
|
||||
return
|
||||
self.last_cmd_output_start_time = 0.
|
||||
try:
|
||||
self.last_cmd_exit_status = int(exit_status)
|
||||
except Exception:
|
||||
self.last_cmd_exit_status = 0
|
||||
end_time = monotonic()
|
||||
last_cmd_output_duration = end_time - self.last_cmd_output_start_time
|
||||
|
||||
self.call_watchers(self.watchers.on_cmd_startstop, {
|
||||
"is_start": False, "time": end_time, 'cmdline': self.last_cmd_cmdline, 'exit_status': self.last_cmd_exit_status})
|
||||
|
||||
opts = get_options()
|
||||
when, duration, action, notify_cmdline = opts.notify_on_cmd_finish
|
||||
|
||||
if last_cmd_output_duration >= duration and when != 'never':
|
||||
cmd = NotificationCommand()
|
||||
cmd.title = 'kitty'
|
||||
s = self.last_cmd_cmdline.replace('\\\n', ' ')
|
||||
cmd.body = f'Command {s} finished with status: {exit_status}.\nClick to focus.'
|
||||
cmd.actions = 'focus'
|
||||
cmd.only_when = OnlyWhen(when)
|
||||
if action == 'notify':
|
||||
notify_with_command(cmd, self.id)
|
||||
elif action == 'bell':
|
||||
def bell(title: str, body: str, identifier: str) -> None:
|
||||
self.screen.bell()
|
||||
notify_with_command(cmd, self.id, notify_implementation=bell)
|
||||
elif action == 'command':
|
||||
def run_command(title: str, body: str, identifier: str) -> None:
|
||||
open_cmd([x.replace('%c', self.last_cmd_cmdline).replace('%s', exit_status) for x in notify_cmdline])
|
||||
notify_with_command(cmd, self.id, notify_implementation=run_command)
|
||||
else:
|
||||
raise ValueError(f'Unknown action in option `notify_on_cmd_finish`: {action}')
|
||||
|
||||
def cmd_output_marking(self, is_start: Optional[bool], cmdline: str = '') -> None:
|
||||
if is_start:
|
||||
start_time = monotonic()
|
||||
self.last_cmd_output_start_time = start_time
|
||||
|
||||
self.call_watchers(self.watchers.on_cmd_startstop, {"is_start": True, "time": start_time})
|
||||
cmdline = decode_cmdline(cmdline) if cmdline else ''
|
||||
self.last_cmd_cmdline = cmdline
|
||||
self.call_watchers(self.watchers.on_cmd_startstop, {"is_start": True, "time": start_time, 'cmdline': cmdline, 'exit_status': 0})
|
||||
else:
|
||||
if self.last_cmd_output_start_time > 0.:
|
||||
end_time = monotonic()
|
||||
last_cmd_output_duration = end_time - self.last_cmd_output_start_time
|
||||
self.last_cmd_output_start_time = 0.
|
||||
|
||||
self.call_watchers(self.watchers.on_cmd_startstop, {"is_start": False, "time": end_time})
|
||||
|
||||
opts = get_options()
|
||||
when, duration, action, cmdline = opts.notify_on_cmd_finish
|
||||
|
||||
if last_cmd_output_duration >= duration and when != 'never':
|
||||
cmd = NotificationCommand()
|
||||
cmd.title = 'kitty'
|
||||
cmd.body = 'Command finished in a background window.\nClick to focus.'
|
||||
cmd.actions = 'focus'
|
||||
cmd.only_when = OnlyWhen(when)
|
||||
if action == 'notify':
|
||||
notify_with_command(cmd, self.id)
|
||||
elif action == 'bell':
|
||||
def bell(title: str, body: str, identifier: str) -> None:
|
||||
self.screen.bell()
|
||||
notify_with_command(cmd, self.id, notify_implementation=bell)
|
||||
elif action == 'command':
|
||||
def run_command(title: str, body: str, identifier: str) -> None:
|
||||
open_cmd(cmdline)
|
||||
notify_with_command(cmd, self.id, notify_implementation=run_command)
|
||||
else:
|
||||
raise ValueError(f'Unknown action in option `notify_on_cmd_finish`: {action}')
|
||||
self.handle_cmd_end(cmdline)
|
||||
# }}}
|
||||
|
||||
# mouse actions {{{
|
||||
|
|
|
|||
|
|
@ -195,6 +195,22 @@ _ksi_main() {
|
|||
_ksi_prompt[ps0_suffix]+="\[\e[0 q\]" # blinking default cursor
|
||||
fi
|
||||
|
||||
_ksi_get_current_command() {
|
||||
builtin local last_cmd
|
||||
last_cmd=$(HISTTIMEFORMAT= builtin history 1)
|
||||
last_cmd="${last_cmd#*[[:digit:]]*[[:space:]]}" # remove leading history number
|
||||
last_cmd="${last_cmd#"${last_cmd%%[![:space:]]*}"}" # remove remaining leading whitespace
|
||||
if [[ "${_ksi_prompt[title]}" == "y" ]]; then
|
||||
builtin printf "\e]2;%s%s\a" "${_ksi_prompt[hostname_prefix]@P}" "${_ksi_prompt[last_cmd]//[[:cntrl:]]}" # remove any control characters
|
||||
fi
|
||||
if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
|
||||
builtin printf "\e]133;C;cmdline="
|
||||
for (( i=0; i<${#last_cmd}; i++ )); do builtin printf '%x ' "'${last_cmd:$i:1}"; done
|
||||
builtin printf "\a"
|
||||
fi
|
||||
}
|
||||
if [[ "${_ksi_prompt[title]}" == "y" || "${_ksi_prompt[mark]}" ]]; then _ksi_prompt[ps0]+='$(_ksi_get_current_command)'; fi
|
||||
|
||||
if [[ "${_ksi_prompt[title]}" == "y" ]]; then
|
||||
if [[ -z "$KITTY_PID" ]]; then
|
||||
if [[ -n "$SSH_TTY" || -n "$SSH2_TTY$KITTY_WINDOW_ID" ]]; then
|
||||
|
|
@ -212,22 +228,16 @@ _ksi_main() {
|
|||
# we use suffix here because some distros add title setting to their bashrc files by default
|
||||
_ksi_prompt[ps1_suffix]+="\[\e]2;${_ksi_prompt[hostname_prefix]}\w\a\]"
|
||||
if [[ "$HISTCONTROL" == *"ignoreboth"* ]] || [[ "$HISTCONTROL" == *"ignorespace"* ]]; then
|
||||
_ksi_debug_print "ignoreboth or ignorespace present in bash HISTCONTROL setting, showing running command in window title will not be robust"
|
||||
_ksi_debug_print "ignoreboth or ignorespace present in bash HISTCONTROL setting, showing running command will not be robust"
|
||||
fi
|
||||
_ksi_get_current_command() {
|
||||
builtin local last_cmd
|
||||
last_cmd=$(HISTTIMEFORMAT= builtin history 1)
|
||||
last_cmd="${last_cmd#*[[:digit:]]*[[:space:]]}" # remove leading history number
|
||||
last_cmd="${last_cmd#"${last_cmd%%[![:space:]]*}"}" # remove remaining leading whitespace
|
||||
builtin printf "\e]2;%s%s\a" "${_ksi_prompt[hostname_prefix]@P}" "${last_cmd//[[:cntrl:]]}" # remove any control characters
|
||||
}
|
||||
_ksi_prompt[ps0_suffix]+='$(_ksi_get_current_command)'
|
||||
fi
|
||||
|
||||
if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
|
||||
_ksi_prompt[ps1]+="\[\e]133;A\a\]"
|
||||
# this can result in multiple D prompt marks or ones that dont
|
||||
# correspond to a cmd but kitty handles this gracefully, only
|
||||
# taking into account the first D after a C.
|
||||
_ksi_prompt[ps1]+="\[\e]133;D;\$?\a\e]133;A\a\]"
|
||||
_ksi_prompt[ps2]+="\[\e]133;A;k=s\a\]"
|
||||
_ksi_prompt[ps0]+="\[\e]133;C\a\]"
|
||||
fi
|
||||
|
||||
builtin alias edit-in-kitty="kitten edit-in-kitty"
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function __ksi_schedule --on-event fish_prompt -d "Setup kitty integration after
|
|||
|
||||
function __ksi_mark_output_start --on-event fish_preexec
|
||||
set --global __ksi_prompt_state pre-exec
|
||||
echo -en "\e]133;C\a"
|
||||
printf '\e]133;C;cmdline_url=%s\a' (string escape --style=url -- "$argv")
|
||||
end
|
||||
|
||||
function __ksi_mark_output_end --on-event fish_postexec
|
||||
|
|
|
|||
|
|
@ -215,7 +215,9 @@ _ksi_deferred_init() {
|
|||
# its preexec hook before us, we'll incorrectly mark its output as
|
||||
# belonging to the command (as if the user typed it into zle) rather
|
||||
# than command output.
|
||||
builtin print -nu $_ksi_fd '\e]133;C\a'
|
||||
builtin print -nu "$_ksi_fd" '\e]133;C;cmdline='
|
||||
for (( i=0; i<${#1}; i++ )); do builtin print -nu "$_ksi_fd" -f '%x ' "'${1:$i:1}"; done
|
||||
builtin print -nu "$_ksi_fd" '\a'
|
||||
(( _ksi_state = 1 ))
|
||||
}
|
||||
|
||||
|
|
@ -401,9 +403,9 @@ _ksi_deferred_init() {
|
|||
[[ "$arg" != -* && "$arg" != *=* ]] && builtin break # command found
|
||||
done
|
||||
if [[ "$is_sudoedit" == "y" ]]; then
|
||||
builtin command sudo "$@";
|
||||
else
|
||||
builtin command sudo TERMINFO="$TERMINFO" "$@";
|
||||
builtin command sudo "$@";
|
||||
else
|
||||
builtin command sudo TERMINFO="$TERMINFO" "$@";
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
|
@ -411,8 +413,8 @@ _ksi_deferred_init() {
|
|||
|
||||
# Map alt+left/right to move by word if not already mapped. This is expected behavior on macOS and I am tired
|
||||
# of answering questions about it.
|
||||
[[ $(builtin bindkey "^[[1;3C") == *" undefined-key" ]] && builtin bindkey "^[[1;3C" "forward-word"
|
||||
[[ $(builtin bindkey "^[[1;3D") == *" undefined-key" ]] && builtin bindkey "^[[1;3D" "backward-word"
|
||||
[[ $(builtin bindkey "^[[1;3C") == *" undefined-key" ]] && builtin bindkey "^[[1;3C" "forward-word"
|
||||
[[ $(builtin bindkey "^[[1;3D") == *" undefined-key" ]] && builtin bindkey "^[[1;3D" "backward-word"
|
||||
|
||||
# Unfunction _ksi_deferred_init to save memory. Don't unfunction
|
||||
# kitty-integration though because decent public functions aren't supposed to
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue