diff --git a/docs/desktop-notifications.rst b/docs/desktop-notifications.rst index 7507843cb..a3e4fd1a7 100644 --- a/docs/desktop-notifications.rst +++ b/docs/desktop-notifications.rst @@ -100,14 +100,6 @@ code. Similarly if values for known keys are unknown, the terminal emulator *should* either ignore the entire escape code or perform a best guess effort to display it based on what it does understand. -.. note:: - It is possible to extend this escape code to allow specifying an icon for - the notification, however, given that some platforms, such as legacy versions - of macOS, don't allow displaying custom images on a notification, it was - decided to leave it out of the spec for the time being. - - Similarly, features such as scheduled notifications could be added in future - revisions. Being informed when a notification is closed ------------------------------------------------ @@ -143,9 +135,10 @@ notifications are still alive (not closed), with:: 99 ; i=myid : p=alive ; The terminal will reply with:: + 99 ; i=myid : p=alive ; id1,id2,id3 -Here, ``myid`` is present for multiplxer support. The reponse from the terminal +Here, ``myid`` is present for multiplexer support. The response from the terminal contains a comma separated list of ids that are still alive. @@ -218,7 +211,7 @@ Key Value query response. ``o`` Comma separated list of occassions from the ``o`` key that the - terminal implements. If no occassions are supported, the value + terminal implements. If no occasions are supported, the value ``o=always`` must be sent in the query response. ``u`` Comma separated list of urgency values that the terminal implements. @@ -236,7 +229,7 @@ Key Value ======= ================================================================================ In the future, if this protocol expands, more keys might be added. Clients must -ignore keys they dont understand in the query response. +ignore keys they do not understand in the query response. To check if a terminal emulator supports this notifications protocol the best way is to send the above *query action* followed by a request for the `primary device @@ -264,7 +257,7 @@ Key Value Default Description otherwise it is plain UTF-8 text with no C0 control codes in it ``i`` ``[a-zA-Z0-9-_+.]`` ``0`` Identifier for the notification. Make these globally unqiue, - like an UUID, so that termial multiplxers can + like an UUID, so that terminal multiplexers can direct responses to the correct window. ``p`` One of ``title``, ``title`` Whether the payload is the notification title or body or query. If a diff --git a/kittens/notify/main.go b/kittens/notify/main.go index e94a1a42f..17b1a3338 100644 --- a/kittens/notify/main.go +++ b/kittens/notify/main.go @@ -1,9 +1,11 @@ package notify import ( + "bytes" "encoding/base64" "fmt" "os" + "slices" "strconv" "strings" "time" @@ -42,7 +44,7 @@ func create_metadata(opts *Options, wait_till_closed bool, expire_time time.Dura ans = append(ans, "t="+b64encode(opts.Type)) } if wait_till_closed { - ans = append(ans, "c=1") + ans = append(ans, "c=1:a=report") } m := strings.Join(ans, ":") if m != "" { @@ -82,11 +84,78 @@ func run_loop(title, body, identifier string, opts *Options, wait_till_closed bo if err != nil { return err } + activated := "" + prefix := ESC_CODE_PREFIX + "i=" + identifier + poll_for_close := func() { + lp.AddTimer(time.Millisecond*50, false, func(_ loop.IdType) error { + lp.QueueWriteString(prefix + ":p=alive;" + ESC_CODE_SUFFIX) + return nil + }) + } lp.OnInitialize = func() (string, error) { generate_chunks(title, body, identifier, opts, wait_till_closed, expire_time, func(x string) { lp.QueueWriteString(x) }) return "", nil } + lp.OnEscapeCode = func(ect loop.EscapeCodeType, data []byte) error { + if ect == loop.OSC && bytes.HasPrefix(data, []byte(ESC_CODE_PREFIX[2:])) { + raw := utils.UnsafeBytesToString(data[len(ESC_CODE_PREFIX[2:]):]) + metadata, payload, _ := strings.Cut(raw, ";") + sent_identifier, payload_type := "", "" + for _, x := range strings.Split(metadata, ":") { + key, val, _ := strings.Cut(x, "=") + switch key { + case "i": + sent_identifier = val + case "p": + payload_type = val + } + } + if sent_identifier == identifier { + switch payload_type { + case "close": + if payload == "untracked" { + poll_for_close() + } else { + lp.Quit(0) + } + case "alive": + live_ids := strings.Split(payload, ",") + if slices.Contains(live_ids, identifier) { + poll_for_close() + } else { + lp.Quit(0) + } + case "": + activated = utils.IfElse(payload == "", "activated", payload) + } + } + } + return nil + } + close_requested := 0 + lp.OnKeyEvent = func(event *loop.KeyEvent) error { + if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") { + event.Handled = true + switch close_requested { + case 0: + lp.QueueWriteString(prefix + ":p=close;" + ESC_CODE_SUFFIX) + lp.Println("Closing notification, please wait...") + close_requested++ + case 1: + key := "Esc" + if event.MatchesPressOrRepeat("ctrl+c") { + key = "Ctrl+C" + } + lp.Println(fmt.Sprintf("Waiting for response from terminal, press the %s key again to abort. Note that this might result in garbage being printed to the terminal.", key)) + close_requested++ + default: + return fmt.Errorf("Aborted by user!") + } + } + return nil + } + err = lp.Run() ds := lp.DeathSignalName() if ds != "" { @@ -94,6 +163,9 @@ func run_loop(title, body, identifier string, opts *Options, wait_till_closed bo lp.KillIfSignalled() return } + if activated != "" && err == nil { + fmt.Println(activated) + } return } diff --git a/kittens/notify/main.py b/kittens/notify/main.py index 31351e5f4..a469b81ae 100644 --- a/kittens/notify/main.py +++ b/kittens/notify/main.py @@ -45,7 +45,8 @@ --wait-till-closed -w type=bool-set Wait until the notification is closed. If the user activates the notification, -"activated" is printed to STDOUT before quitting. +"activated" is printed to STDOUT before quitting. Press the Esc or Ctrl+C keys +to close the notification manually. --only-print-escape-code