diff --git a/docs/desktop-notifications.rst b/docs/desktop-notifications.rst index 1260bdb32..7507843cb 100644 --- a/docs/desktop-notifications.rst +++ b/docs/desktop-notifications.rst @@ -51,7 +51,7 @@ the notification is done, and should be displayed. You can specify the title or body multiple times and the terminal emulator will concatenate them, thereby allowing arbitrarily long text (terminal emulators are free to impose a sensible limit to avoid Denial-of-Service attacks). The size of the payload must be no -longer than ``2048`` bytes, *before being encoded*. +longer than ``4096`` bytes, *before being encoded*. Both the ``title`` and ``body`` payloads must be either UTF-8 encoded plain text with no embedded escape codes, or UTF-8 text that is :rfc:`base64 <4648>` diff --git a/kittens/notify/main.go b/kittens/notify/main.go index e5073b2fb..9c25355b8 100644 --- a/kittens/notify/main.go +++ b/kittens/notify/main.go @@ -1,14 +1,128 @@ package notify import ( + "encoding/base64" "fmt" + "os" + "strconv" + "strings" "kitty/tools/cli" + "kitty/tools/tui/loop" + "kitty/tools/utils" ) var _ = fmt.Print -func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) { +const ESC_CODE_PREFIX = "\x1b]99;" +const ESC_CODE_SUFFIX = "\x1b\\" +const CHUNK_SIZE = 4096 + +func b64encode(x string) string { + return base64.RawStdEncoding.EncodeToString(utils.UnsafeStringToBytes(x)) +} + +func create_metadata(opts *Options) string { + ans := []string{} + if opts.AppName != "" { + ans = append(ans, "f="+b64encode(opts.AppName)) + } + switch opts.Urgency { + case "low": + ans = append(ans, "u=0") + case "critical": + ans = append(ans, "u=2") + } + if opts.ExpireTime >= 0 { + ans = append(ans, "w="+strconv.Itoa(opts.ExpireTime)) + } + if opts.Type != "" { + ans = append(ans, "t="+b64encode(opts.Type)) + } + if opts.WaitTillClosed { + ans = append(ans, "c=1") + } + m := strings.Join(ans, ":") + if m != "" { + m = ":" + m + } + return m +} + +func generate_chunks(title, body, identifier string, opts *Options, wait_till_closed bool, callback func(string)) { + prefix := ESC_CODE_PREFIX + "i=" + identifier + write_chunk := func(middle string) { + callback(prefix + middle + ESC_CODE_SUFFIX) + } + + add_payload := func(payload_type, payload string) { + p := utils.IfElse(payload_type == "title", "", ":p="+payload_type) + for len(payload) > 0 { + chunk := payload[:min(CHUNK_SIZE, len(payload))] + payload = utils.IfElse(len(payload) > len(chunk), payload[len(chunk):], "") + enc := b64encode(chunk) + write_chunk(":d=0" + p + ";" + enc) + } + } + metadata := create_metadata(opts) + write_chunk(":d=0" + metadata + ";") + add_payload("title", title) + if body != "" { + add_payload("body", body) + } + write_chunk(";") +} + +func run_loop(title, body, identifier string, opts *Options, wait_till_closed bool) (err error) { + lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking) + if err != nil { + return err + } + + lp.OnInitialize = func() (string, error) { + generate_chunks(title, body, identifier, opts, wait_till_closed, func(x string) { lp.QueueWriteString(x) }) + return "", nil + } +} + +func random_ident() (string, error) { + return utils.HumanUUID4() +} + +func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) { + if len(args) == 0 { + return 1, fmt.Errorf("Must specify a TITLE for the notification") + } + title := args[0] + if len(title) == 0 { + return 1, fmt.Errorf("Must specify a non-empty TITLE for the notification") + } + body := "" + if len(args) > 1 { + body = strings.Join(args[1:], " ") + } + ident := opts.Identifier + if ident == "" { + if ident, err = random_ident(); err != nil { + return 1, fmt.Errorf("Failed to generate a random identifier with error: %w", err) + } + } + wait_till_closed := opts.WaitTillClosed + if opts.OnlyPrintEscapeCode { + generate_chunks(title, body, ident, opts, wait_till_closed, func(x string) { + if err == nil { + _, err = os.Stdout.WriteString(x) + } + }) + } else { + if opts.PrintIdentifier { + fmt.Println(ident) + } + err = run_loop(title, body, ident, opts, wait_till_closed) + } + if err != nil { + rc = 1 + } return } diff --git a/kittens/notify/main.py b/kittens/notify/main.py index 9edcac337..0b5a598a4 100644 --- a/kittens/notify/main.py +++ b/kittens/notify/main.py @@ -37,6 +37,7 @@ --print-identifier -p +type=bool-set Print the identifier for the notification to STDOUT. Useful when not specifying your own identifier via the --identifier option. @@ -45,6 +46,13 @@ type=bool-set Wait until the notification is closed. If the user activates the notification, "activated" is printed to STDOUT before quitting. + + +--only-print-escape-code +type=bool-set +Only print the escape code to STDOUT. Useful if using this kitten as part +of a larger application. If this is specified, the --wait-till-closed option +will be used for escape code generation, but no actual waiting will be done. '''.format help_text = '''\ Send notifications to the user that are displayed to them via the