A collection of (ANSI-sequence aware) text reflow operations & algorithms

reflow

Latest Release Build Status Coverage Status Go ReportCard GoDoc

A collection of ANSI-aware methods and io.Writers helping you to transform blocks of text. This means you can still style your terminal output with ANSI escape sequences without them affecting the reflow operations & algorithms.

Word-Wrapping

The wordwrap package lets you word-wrap strings or entire blocks of text.

import "github.com/muesli/reflow/wordwrap"

s := wordwrap.String("Hello World!", 5)
fmt.Println(s)

Result:

Hello
World!

The word-wrapping Writer is compatible with the io.Writer / io.WriteCloser interfaces:

f := wordwrap.NewWriter(limit)
f.Write(b)
f.Close()

fmt.Println(f.String())

Customize word-wrapping behavior:

f := wordwrap.NewWriter(limit)
f.Breakpoints = []rune{':', ','}
f.Newline = []rune{'\r'}

Unconditional Wrapping

The wrap package lets you unconditionally wrap strings or entire blocks of text.

import "github.com/muesli/reflow/wrap"

s := wrap.String("Hello World!", 7)
fmt.Println(s)

Result:

Hello W
orld!

The unconditional wrapping Writer is compatible with the io.Writer interfaces:

f := wrap.NewWriter(limit)
f.Write(b)

fmt.Println(f.String())

Customize word-wrapping behavior:

f := wrap.NewWriter(limit)
f.Newline = []rune{'\r'}
f.KeepNewlines = false
f.reserveSpace = true
f.TabWidth = 2

Tip: This wrapping method can be used in conjunction with word-wrapping when word-wrapping is preferred but a line limit has to be enforced:

wrapped := wrap.String(wordwrap.String("Just an example", 5), 5)
fmt.Println(wrapped)

Result:

Just
an
examp
le

ANSI Example

s := wordwrap.String("I really \x1B[38;2;249;38;114mlove\x1B[0m Go!", 8)
fmt.Println(s)

Result:

ANSI Example Output

Indentation

The indent package lets you indent strings or entire blocks of text.

import "github.com/muesli/reflow/indent"

s := indent.String("Hello World!", 4)
fmt.Println(s)

Result: Hello World!

There is also an indenting Writer, which is compatible with the io.Writer interface:

// indent uses spaces per default:
f := indent.NewWriter(width, nil)

// but you can also use a custom indentation function:
f = indent.NewWriter(width, func(w io.Writer) {
    w.Write([]byte("."))
})

f.Write(b)
f.Close()

fmt.Println(f.String())

Dedentation

The dedent package lets you dedent strings or entire blocks of text.

import "github.com/muesli/reflow/dedent"

input := `    Hello World!
  Hello World!
`

s := dedent.String(input)
fmt.Println(s)

Result:

  Hello World!
Hello World!

Padding

The padding package lets you pad strings or entire blocks of text.

import "github.com/muesli/reflow/padding"

s := padding.String("Hello", 8)
fmt.Println(s)

Result: Hello___ (the underlined portion represents 3 spaces)

There is also a padding Writer, which is compatible with the io.WriteCloser interface:

// padding uses spaces per default:
f := padding.NewWriter(width, nil)

// but you can also use a custom padding function:
f = padding.NewWriter(width, func(w io.Writer) {
    w.Write([]byte("."))
})

f.Write(b)
f.Close()

fmt.Println(f.String())
Owner
Christian Muehlhaeuser
Geek, Gopher, Software Developer, Maker, Opensource Advocate, Tech Enthusiast, Photographer, Board and Card Gamer
Christian Muehlhaeuser
Comments
  • Add a new method `Flush` to reuse writer

    Add a new method `Flush` to reuse writer

    This PR add a new method Flush to reuse writer. Here is the code snippet

    package main
    
    import (
    	"fmt"
    	"io"
    
    	"github.com/muesli/reflow/padding"
    )
    
    func main() {
    	f := padding.NewWriter(6, func(w io.Writer) {
    		_, _ = w.Write([]byte("."))
    	})
    	f.Write([]byte("foo\nbar"))
    	f.Flush()
    	// foo...\nbar...
    	fmt.Println(f.String())
    
    	f.Write([]byte("bar\nbaz"))
    	f.Flush()
    	// bar...\nbaz...
    	fmt.Println(f.String())
    }
    

    Further more, when writer closed, it shouldn't be used again according to #13(comment).

    Btw I'm not sure the descriptions about Flush and Close conform to your standard. So if you any ideas, please let me know.

  • New optimized write performance seems to break glamor

    New optimized write performance seems to break glamor

    The recent change (in ansi.Writer) to iterating over bytes instead of runes seems to break glamor's UTF-8 processing here: https://github.com/charmbracelet/glamour/blob/master/ansi/margin.go

    The first indent pipe (using ansi.Writer) breaks up UTF-8 byte sequences into single byte writes, which are misinterpreted when ingested into the second padding pipe, as they go through a byte->string->[]byte conversion.

    Here's an illustration of the effect: https://play.golang.org/p/N-caKzuAwdQ

    Originally posted by @zavislak in https://github.com/muesli/reflow/pull/10#r510675805

  • Add unconditional wrapping

    Add unconditional wrapping

    This PR adds unconditional wrapping where each line is ended as soon as the limit is reached. The code is tested and documented in README.md. The PR also closes #29.

    fmt.Println(wrap.String("Hello World!", 7))
    // Hello W
    // orld!
    

    This also works great in conjunction with word-wrapping when word-wrapping is preferred but the limit is mandatory (e.g. terminal width):

    fmt.Println(wrap.String(wordwrap.String("Just an example", 5), 5))
    // Just
    // an
    // examp
    // le
    

    The unconditional wrapper also allows for some configuration:

    • Newline: Like in word-wrap
    • KeepNewlines: Like in word-wrap
    • PreserveSpace: Whether leading spaces in new lines should be ignored
    • TabWidth: Width of tabs when expresses as spaces (because respecting the limit is impossible otherwise)

    Also I noticed a common pattern when checking ANSI markers, so I added helpers to the ansi package. These can also be applied to the other algorithms in another PR.

  • Implement an ANSI-aware string truncation function

    Implement an ANSI-aware string truncation function

    Tests are also included. Also, I haven't implemented this on Buffer yet.

    I abstracted out the code for detecting ANSI start and end sequences for sake of reuse, which impacts performance very slightly (thanks @kiyonlin for the benchmarking tests). We could instead manually inline that function if we wanted for a slight performance boost.

    This closes #21.

  • Dedentation package

    Dedentation package

    This adds a dedent package. Closes #5.

    Instead of the implementation provided in the issue, I use a strings.Builder for Go 1.10 and onwards, which was a very easy performance improvement:

    name      old time/op    new time/op    delta
    Dedent-8     835ns ± 3%     656ns ± 8%  -21.50%  (p=0.000 n=9+10)
    
    name      old alloc/op   new alloc/op   delta
    Dedent-8      344B ± 0%      328B ± 0%   -4.65%  (p=0.000 n=10+10)
    
    name      old allocs/op  new allocs/op  delta
    Dedent-8      11.0 ± 0%       9.0 ± 0%  -18.18%  (p=0.000 n=10+10)
    
  • Can't use padding Writer instance twice

    Can't use padding Writer instance twice

    I don't know if it is a bug or an intended/designed choose.

    package main
    
    import (
    	"fmt"
    	"io"
    
    	"github.com/muesli/reflow/padding"
    )
    
    func main() {
    	width := uint(6)
    	b := []byte("foo")
    
    	// padding uses spaces per default:
    	f := padding.NewWriter(width, nil)
    
    	// but you can also use a custom padding function:
    	f = padding.NewWriter(width, func(w io.Writer) {
    		w.Write([]byte("."))
    	})
    
    	f.Write(b)
    	f.Close()
    	
    	// foo...
    	fmt.Println(f.String())
    
    	f.Write(b)
    	f.Close()
    
    	// foo...foo
    	fmt.Println(f.String())
    }
    
  • Working with tabs?

    Working with tabs?

    Hello and thank you for making this! I was just wondering if you have a solution for tabs, which reflow seems to count as one column, but their width is really dependent on the terminal and their position with the tabstop.

    For now I've got my pager program replacing tabs with eight spaces but it's not ideal for the navigation and copy pasting. I'm not sure how programs like Vim (or the libraries) cope with tabs and truncating etc.

    Thanks again, John

  • Bump golangci/golangci-lint-action from 2 to 3

    Bump golangci/golangci-lint-action from 2 to 3

    Bumps golangci/golangci-lint-action from 2 to 3.

    Release notes

    Sourced from golangci/golangci-lint-action's releases.

    v3.0.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/golangci/golangci-lint-action/compare/v2...v3.0.0

    Bump version v2.5.2

    Bug fixes

    • 5c56cd6 Extract and don't mangle User Args. (#200)

    Dependencies

    • e3c53fe bump @​typescript-eslint/eslint-plugin (#194)
    • 3b9f80e bump @​typescript-eslint/parser from 4.18.0 to 4.19.0 (#195)
    • 9845713 bump @​types/node from 14.14.35 to 14.14.37 (#197)
    • e789ee1 bump eslint from 7.22.0 to 7.23.0 (#196)
    • f2e9a96 bump @​typescript-eslint/eslint-plugin (#188)
    • 818081a bump @​types/node from 14.14.34 to 14.14.35 (#189)
    • 6671836 bump @​typescript-eslint/parser from 4.17.0 to 4.18.0 (#190)
    • 526907e bump @​typescript-eslint/parser from 4.16.1 to 4.17.0 (#185)
    • 6b6ba16 bump @​typescript-eslint/eslint-plugin (#186)
    • 9cab4ef bump eslint from 7.21.0 to 7.22.0 (#187)
    • 0c76572 bump @​types/node from 14.14.32 to 14.14.34 (#184)
    • 0dfde21 bump @​typescript-eslint/parser from 4.15.2 to 4.16.1 (#182)
    • 9dcf389 bump typescript from 4.2.2 to 4.2.3 (#181)
    • 34d3904 bump @​types/node from 14.14.31 to 14.14.32 (#180)
    • e30b22f bump @​typescript-eslint/eslint-plugin (#179)
    • 8f30d25 bump eslint from 7.20.0 to 7.21.0 (#177)
    • 0b64a40 bump @​typescript-eslint/parser from 4.15.1 to 4.15.2 (#176)
    • 973b3a3 bump eslint-config-prettier from 8.0.0 to 8.1.0 (#178)
    • 6ea3de1 bump @​typescript-eslint/eslint-plugin (#175)
    • 6eec6af bump typescript from 4.1.5 to 4.2.2 (#174)

    v2.5.1

    Bug fixes:

    • d9f0e73 Check that go.mod exists in reading the version (#173)

    v2.5.0

    New Features:

    • 51485a4 Try to get version from go.mod file (#118)

    ... (truncated)

    Commits
    • c675eb7 Update all direct dependencies (#404)
    • 423fbaf Remove Setup-Go (#403)
    • bcfc6f9 build(deps-dev): bump eslint-plugin-import from 2.25.3 to 2.25.4 (#402)
    • d34ac2a build(deps): bump setup-go from v2.1.4 to v2.2.0 (#401)
    • e4b538e build(deps-dev): bump @​types/node from 16.11.10 to 17.0.19 (#400)
    • a288c0d build(deps): bump @​actions/cache from 1.0.8 to 1.0.9 (#399)
    • b7a34f8 build(deps): bump @​types/tmp from 0.2.2 to 0.2.3 (#398)
    • 129bcf9 build(deps-dev): bump @​types/uuid from 8.3.3 to 8.3.4 (#397)
    • 153576c build(deps-dev): bump eslint-config-prettier from 8.3.0 to 8.4.0 (#396)
    • a9a9dff build(deps): bump ansi-regex from 5.0.0 to 5.0.1 (#395)
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • BytesWithTail and StringsWithTail should put tail between escape sequences

    BytesWithTail and StringsWithTail should put tail between escape sequences

    When a trail is added to stylized (e.g. color) text, it would be great if the tail (e.g. ellipsis) was included in the sequence.

    For example, consider the string: \x1b[0;31mexample\x1b[0m. If part of the string is replaced, it's the string within the vterm sequence that is replaced; thus, the ellipsis should be before the vterm sequence as well: \x1b[0;31mex...\x1b[0m

  • Use ansi package to find ANSI marker/terminators in truncate package

    Use ansi package to find ANSI marker/terminators in truncate package

    This PR updates the truncate package to use the ansi package to detect the presence of ANSI sequences. Currently the truncate package re-implements the detection.

  • More truncate tests + bump go-runewidth

    More truncate tests + bump go-runewidth

    This PR adds additional tests to verify some of the truncate package's idiosyncrasies as well as verify some of the behavior we're inheriting from go-runewidth.

    This PR also bumps go-runewidth to the current version which contains improvements to width detection (see: mattn/go-runewidth#29).

  • Don't wrap at non breaking space.

    Don't wrap at non breaking space.

    unicode.IsSpace also returns true for a non breaking space (0xA0), which isn't supposed to be treated as a space in this context.

    https://github.com/muesli/reflow/blob/00a9f5c6902562434539e11d2c8f8d3dae851318/wordwrap/wordwrap.go#L127

  • Hyperlinks consume space though the link isn't printed

    Hyperlinks consume space though the link isn't printed

    I was modifying glamour to output links without the anchor text, but this uses up "space" in wordwrap.go even though it ideally would know that the anchor text does not get displayed by the terminal.

    Here's a printf example that you can use to see what the link output looks like:

    $ printf '\e]8;;file:./SECURITY.md\e\\This is a link\e]8;;\e\\\n'
    

    So the bytes from file:./SECURITY.md should not be counted towards the block's wordwrapping. Here's an example of how it fails to look correct:

    $ ./glow ~/projects/kitty/CONTRIBUTING.md 
    
      ### Reporting bugs                                                                                
                                                                                                        
      Please first search existing bug reports (especially closed ones) for a report that matches your  
      issue.                                                                                            
                                                                                                      
      When reporting a bug, provide full details of your environment, that means, at a minimum, kitty   
      version, OS and OS version, kitty config (ideally a minimal config to reproduce the issue with).  
                                                                                                        
      ### Contributing code                                                                             
                                                                                                        
      Install the dependencies using your favorite  
      package manager. Build and run kitty from source.                                                                                
                                                                                                      
      Make a fork, submit your Pull Request. If it's a large/controversial change, open an issue        
      beforehand to discuss it, so that you don't waste your time making a pull request that gets       
      rejected.                                                                                         
                                                                                                      
      If the code you are submitting is reasonably easily testable, please contribute tests as well (see
      the  kitty_tests/  sub-directory for existing tests, which can be run with  ./test.py ).          
                                                                                                      
      That's it.                                                                                        
    

    Note how the middle paragraph that contains a link for "the dependencies" and a link for "from source" cause the paragraph to be badly wrapped, the same as if the URL was inline:

    
    $ glow ~/projects/kitty/CONTRIBUTING.md 
    
      ### Reporting bugs                                                                                
                                                                                                        
      Please first search existing bug reports (especially closed ones) for a report that matches your  
      issue.                                                                                            
                                                                                                      
      When reporting a bug, provide full details of your environment, that means, at a minimum, kitty   
      version, OS and OS version, kitty config (ideally a minimal config to reproduce the issue with).  
                                                                                                        
      ### Contributing code                                                                             
                                                                                                        
      Install the dependencies https://sw.kovidgoyal.net/kitty/build/#dependencies using your favorite  
      package manager. Build and run kitty from source https://sw.kovidgoyal.net/kitty/build/#install-and-
      run-from-source.                                                                                  
                                                                                                      
      Make a fork, submit your Pull Request. If it's a large/controversial change, open an issue        
      beforehand to discuss it, so that you don't waste your time making a pull request that gets       
      rejected.                                                                                         
                                                                                                      
      If the code you are submitting is reasonably easily testable, please contribute tests as well (see
      the  kitty_tests/  sub-directory for existing tests, which can be run with  ./test.py ).          
                                                                                                      
      That's it.                                                                                        
    
  • v0.3.0 broke dedent package

    v0.3.0 broke dedent package

    Assume the following (in real life dynamically created) string:

    Your bank accounts:
    
      DKB
        ✓ Logged in as jamesbond
      ING
        ✓ Logged in as jamesbond
    

    The indentation is there for a reason. At one stage a special character is inserted to denote the selected bank account (if an account is actually selected):

    Your bank accounts:
    
    ➜ DKB
        ✓ Logged in as jamesbond
      ING
        ✓ Logged in as jamesbond
    

    Regardless if an account is selected or not, this is run through dedent.String() to get rid of the indentation of the first example above.

    With v0.2.0 and v0.3.0, this works fine:

    Your bank accounts:
    
    DKB
      ✓ Logged in as jamesbond
    ING
      ✓ Logged in as jamesbond
    

    However, when the arrow is present as in the second example above, dedentation gets mangled with v0.3.0:

    Your bank accounts:
    
    ➜DKB
      ✓ Logged in as jamesbond
    ING
      ✓ Logged in as jamesbond
    

    Not even the whitespace between the char and the next string is preserved.

    To sum this up and give a working example, try the following code with v0.2.0 and v0.3.0:

    const s = `Your bank accounts:
    
    ➜ DKB
        ✓ Logged in as jamesbond
      ING
        ✓ Logged in as jamesbond
    `
    
    fmt.Print(dedent.String(s))
    
  • Extra newlines when combining unconditional and standard word wrapping

    Extra newlines when combining unconditional and standard word wrapping

    When combining word-wrapping with unconditional wrapping as described in the README, extra linebreaks can sometimes be found in the output.

    For example:

    const str = "the quick brown foxxxxxxxxxxxxxxxx jumped over the lazy dog."
    const limit = 16
    wrapped := wrap.String(wordwrap.String(str, limit), limit)
    fmt.Println(wrapped)
    

    Outputs:

    the quick brown
    foxxxxxxxxxxxxxx
    xx
    jumped over the
    lazy dog.
    

    However, I'd expect it to be:

    the quick brown
    foxxxxxxxxxxxxxx
    xx jumped over 
    the lazy dog.
    

    Playground Example

  • Issue with wrapping CP437 ANSI file

    Issue with wrapping CP437 ANSI file

    I'm attempting to wrap a CP437 ANSI file, with a wrap after column 80. However, something is causing to to not wrap correctly.

    I'm decoding before using reflow, and then re-encoding CP437 afterward, and printing...

    when using reflow: rendered

    the actual art file for reference: art

    Not sure if there's something obvious I'm missing...? Happy to take to SO if this is not the forum for this type of question.

    Thanks for a great library!

    (code for reference)

  • Consistency with widths: int vs uint

    Consistency with widths: int vs uint

    I've noticed, while using various parts Reflow together, that most packages int to describe widths, while the newer truncate package uses uint. For example:

    // In the ansi sub-package
    func PrintableRuneWidth(s string) int
    
    // In the truncate sub-package
    func String(s string, width uint) string
    

    Why not standardize on or the other? If not breaking the API is a concern, then it would be safer to stay with int as the truncate package is still unreleased.

a tool to output images as RGB ANSI graphics on the terminal
a tool to output images as RGB ANSI graphics on the terminal

imgcat Tool to output images in the terminal. Built with bubbletea install homebrew brew install trashhalo/homebrew-brews/imgcat prebuilt packages Pr

Dec 28, 2022
Content aware image resize library
Content aware image resize library

Caire is a content aware image resize library based on Seam Carving for Content-Aware Image Resizing paper. How does it work An energy map (edge detec

Jan 2, 2023
Image processing algorithms in pure Go
Image processing algorithms in pure Go

bild A collection of parallel image processing algorithms in pure Go. The aim of this project is simplicity in use and development over absolute high

Jan 6, 2023
Turn asterisk-indented text lines into mind maps
Turn asterisk-indented text lines into mind maps

Crumbs Turn asterisk-indented text lines into mind maps. Organize your notes in a hierarchical tree structure, using a simple text editor. an asterisk

Jan 8, 2023
API-first image file text search 🔍

API-first image file text search ??

Dec 11, 2021
Raw ANSI sequence helpers

Raw ANSI sequence helpers

Oct 23, 2022
Golang terminal ANSI OSC52 wrapper. Copy text to clipboard from anywhere.

go-osc52 A terminal Go library to copy text to clipboard from anywhere. It does so using ANSI OSC52. The Copy() function defaults to copying text from

Dec 6, 2022
subtraction operations and also parentheses to indicate order of operations

basic parsing expose a Calculate method that accepts a string of addition / subtraction operations and also parentheses to indicate order of operation

Feb 22, 2022
Distributed File Store Application Consist of API Server to handle file operations and command line tool to do operations

Filestore Distributed File Store Application Consist of API Server to handle file operations and command line tool to do operations (store named binar

Nov 7, 2021
Golang string comparison and edit distance algorithms library, featuring : Levenshtein, LCS, Hamming, Damerau levenshtein (OSA and Adjacent transpositions algorithms), Jaro-Winkler, Cosine, etc...

Go-edlib : Edit distance and string comparison library Golang string comparison and edit distance algorithms library featuring : Levenshtein, LCS, Ham

Dec 20, 2022
Golang string comparison and edit distance algorithms library, featuring : Levenshtein, LCS, Hamming, Damerau levenshtein (OSA and Adjacent transpositions algorithms), Jaro-Winkler, Cosine, etc...

Go-edlib : Edit distance and string comparison library Golang string comparison and edit distance algorithms library featuring : Levenshtein, LCS, Ham

Dec 20, 2022
Go translations of the algorithms and clients in the textbook Algorithms, 4th Edition by Robert Sedgewick and Kevin Wayne.

Overview Go translations of the Java source code for the algorithms and clients in the textbook Algorithms, 4th Edition by Robert Sedgewick and Kevin

Dec 13, 2022
fim is a collection of some popular frequent itemset mining algorithms implemented in Go.

fim fim is a collection of some popular frequent itemset mining algorithms implemented in Go. fim contains the implementations of the following algori

Jul 14, 2022
Skillshot - A collection of ranking algorithms to be used in matchmaking

Skillshot A collection of ranking algorithms to be used in matchmaking. Openskil

Aug 26, 2022
A collection of route planning algorithms for road networks.

route-planning A collection of route planning algorithms for road networks. This collection contains different route planning techniques from a lectur

Jan 29, 2022
Sequence-based Go-native audio mixer for music apps

Mix https://github.com/go-mix/mix Sequence-based Go-native audio mixer for music apps See demo/demo.go: package main import ( "fmt" "os" "time"

Dec 1, 2022
PiHex Library, written in Go, generates a hexadecimal number sequence in the number Pi in the range from 0 to 10,000,000.

PiHex PiHex Library generates a hexadecimal number sequence in the number Pi in the range from 0 to 1.0e10000000. To calculate using "Bailey-Borwein-P

Nov 18, 2022
elPrep: a high-performance tool for analyzing sequence alignment/map files in sequencing pipelines.
elPrep: a high-performance tool for analyzing sequence alignment/map files in sequencing pipelines.

Overview elPrep is a high-performance tool for analyzing .sam/.bam files (up to and including variant calling) in sequencing pipelines. The key advant

Nov 2, 2022
A command line tool to generate sequence diagrams
A command line tool to generate sequence diagrams

goseq - text based sequence diagrams A small command line utility used to generate UML sequence diagrams from a text-base definition file. Inspired by

Dec 22, 2022
Converts a trace of Datadog to a sequence diagram of PlantUML (Currently, supports only gRPC)
Converts a trace of Datadog to a sequence diagram of PlantUML (Currently, supports only gRPC)

jigsaw Automatically generate a sequence diagram from JSON of Trace in Datadog. ⚠️ Only gRPC calls appear in the sequence diagram. Example w/ response

Jul 12, 2022