TUTORIAL.md

  1# Hello World!
  2
  3This is a simple tutorial on how to create a UV application that displays
  4"Hello, World!" on the screen. UV is a terminal UI toolkit that allows you to
  5create terminal applications with ease. It provides a simple API to handle
  6screen management, input handling, and rendering content.
  7
  8## Tutorial
  9
 10What does UV consist of? A UV application consists of a screen that displays
 11content, has some sort of input sources that can be used to interact with the
 12screen, and is meant to display content or programs on the screen.
 13
 14First, we need to create a screen that will display our program. UV comes with
 15a `Terminal` screen that is used to display content on a terminal.
 16
 17```go
 18t := uv.NewTerminal(os.Stdin, os.Stdout, os.Environ())
 19// Or simply use...
 20// t := uv.DefaultTerminal()
 21```
 22
 23A terminal screen has a few properties that are unique to it. For example, a
 24terminal screen can go into raw mode, which is important to disable echoing of
 25input characters, and to disable signal handling so that we can receive things
 26like <kbd>ctrl+c</kbd> without the terminal interfering with our program.
 27
 28Another important property of a terminal screen is the alternate screen buffer.
 29This property puts the terminal screen into a special mode that allows us to
 30display content without interfering with the normal screen buffer.
 31
 32In this tutorial, we will use the alternate screen buffer to display our
 33program so that we don't affect the normal screen buffer.
 34
 35```go
 36// Set the terminal to raw mode.
 37if err := t.MakeRaw(); err != nil {
 38  log.Fatal(err)
 39}
 40
 41// Enter the alternate screen buffer. This will
 42// only take affect once we flush or display
 43// our program on the terminal screen.
 44t.EnterAltScreen()
 45
 46// My program
 47// ...
 48
 49// Make sure we leave the alternate screen buffer when we
 50// are done with our program. This will be called automatically
 51// when we use [t.Shutdown(ctx)] later.
 52t.LeaveAltScreen()
 53
 54// Make sure we restore the terminal to its original state
 55// before we exit. We don't care about errors here, but you
 56// can handle them if you want. This will be called automatically
 57// when we use [t.Shutdown(ctx)] later.
 58_ = t.Restore() //nolint:errcheck
 59```
 60
 61Now that we have our screen set to raw mode and in the alternate screen buffer,
 62we can create our program that will be displayed on the screen. A program is an
 63abstraction layer that handles different screen types and implementations. It
 64only cares about displaying content on the screen.
 65
 66We need to start our program before we can display anything on the screen. This
 67will ensure that the program and screen are initialized and ready to display
 68content. Internally, this will also call `t.Start()` to start the terminal
 69screen.
 70
 71```go
 72if err := t.Start(); err != nil {
 73  log.Fatalf("failed to start program: %v", err)
 74}
 75```
 76
 77Let's display a simple frame with some text in it. A frame is a container that
 78holds the buffer we're displaying. The final cursor position we want our cursor
 79to be at, and the viewport area we are working with to display our content.
 80
 81```go
 82for i, r := range "Hello, World!" {
 83  // We iterate over the string to display each character
 84  // in a separate cell. Ideally, we want each cell
 85  // to have exactly one grapheme. In this case, since
 86  // we're using a simple ASCII string, we know that
 87  // each character is a single grapheme with a width of 1.
 88  var c uv.Cell
 89  c.Content = string(r)
 90  c.Width = 1
 91  t.SetCell(i, 0, &c)
 92}
 93// Now we simply render the changes and flush them
 94// to the terminal screen.
 95_ = p.Display()
 96```
 97
 98Different screen models have different ways to receive input. Some models have
 99a remote control, while others have a touch screen. A terminal can receive
100input from various peripherals usually through control codes and escape
101sequences. Our terminal has a `t.Events(ctx)` method that returns a channel
102which will receive events from different terminal input sources.
103
104```go
105// We want to be able to stop the terminal input loop
106// whenever we call cancel().
107ctx, cancel := context.WithCancel(context.Background())
108defer cancel()
109
110for ev := range t.Events(ctx) {
111  switch ev := ev.(type) {
112  case uv.WindowSizeEvent:
113    // Our terminal screen is resizable. This is important
114    // as we want to inform our terminal screen with the
115    // size we'd like to display our program in.
116    // When we're using the full terminal window size,
117    // we can assume that the terminal screen will
118    // also have the same size as our program.
119    // However, with inline programs, usually we want
120    // the height to be the height of our program.
121    // So if our inline program takes 10 lines, we
122    // want to resize the terminal screen to 10 lines
123    // high.
124    width, height := ev.Width, ev.Height
125    if !altscreen {
126      height = 10
127    }
128    t.Resize(width, height)
129  case uv.KeyPressEvent:
130    if ev.MatchStrings("q", "ctrl+c") {
131      // This will stop the input loop and cancel the context.
132      cancel()
133    }
134  }
135}
136```
137
138Now that we've handled displaying our program and receiving input from the
139terminal, we need to handle the program's lifecycle. We need to make sure that
140we restore the terminal to its original state when we exit the program. A
141terminal program can be stopped gracefully using the `t.Shutdown(ctx)` method.
142
143```go
144// We need to make sure we stop the program gracefully
145// after we exit the input loop.
146if err := t.Shutdown(ctx); err != nil {
147  log.Fatal(err)
148}
149```
150
151Finally, let's put everything together and create a simple program that displays
152a frame with "Hello, World!" in it. The program will exit when we press
153<kbd>ctrl+c</kbd> or <kbd>q</kbd>.
154
155```go
156func main() {
157	t := uv.NewTerminal(os.Stdin, os.Stdout, os.Environ())
158
159	if err := t.MakeRaw(); err != nil {
160		log.Fatalf("failed to make terminal raw: %v", err)
161	}
162
163	if err := t.Start(); err != nil {
164		log.Fatalf("failed to start program: %v", err)
165	}
166
167	t.EnterAltScreen()
168
169	ctx, cancel := context.WithCancel(context.Background())
170	defer cancel()
171
172	for ev := range t.Events(ctx) {
173		switch ev := ev.(type) {
174		case uv.WindowSizeEvent:
175			width, height := ev.Width, ev.Height
176			t.Erase()
177			t.Resize(width, height)
178		case uv.KeyPressEvent:
179			if ev.MatchStrings("q", "ctrl+c") {
180				cancel()
181			}
182		}
183
184		for i, r := range "Hello, World!" {
185			var c uv.Cell
186			c.Content = string(r)
187			c.Width = 1
188			t.SetCell(i, 0, &c)
189		}
190		if err := t.Display(); err != nil {
191			log.Fatal(err)
192		}
193	}
194
195	if err := t.Shutdown(ctx); err != nil {
196		log.Fatal(err)
197	}
198}
199```
200
201---
202
203Part of [Charm](https://charm.sh).
204
205<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
206
207Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة