| 1 | + | // Copyright 2020 Google Inc. |
| 2 | + | // |
| 3 | + | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | + | // you may not use this file except in compliance with the License. |
| 5 | + | // You may obtain a copy of the License at |
| 6 | + | // |
| 7 | + | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | + | // |
| 9 | + | // Unless required by applicable law or agreed to in writing, software |
| 10 | + | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | + | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | + | // See the License for the specific language governing permissions and |
| 13 | + | // limitations under the License. |
| 14 | + | |
| 15 | + | // Binary formdemo creates a form that accepts text inputs and supports |
| 16 | + | // keyboard navigation. |
| 17 | + | package main |
| 18 | + | |
| 19 | + | import ( |
| 20 | + | "context" |
| 21 | + | "fmt" |
| 22 | + | "os/user" |
| 23 | + | "time" |
| 24 | + | |
| 25 | + | "github.com/mum4k/termdash" |
| 26 | + | "github.com/mum4k/termdash/align" |
| 27 | + | "github.com/mum4k/termdash/cell" |
| 28 | + | "github.com/mum4k/termdash/container" |
| 29 | + | "github.com/mum4k/termdash/keyboard" |
| 30 | + | "github.com/mum4k/termdash/linestyle" |
| 31 | + | "github.com/mum4k/termdash/terminal/tcell" |
| 32 | + | "github.com/mum4k/termdash/widgets/button" |
| 33 | + | "github.com/mum4k/termdash/widgets/text" |
| 34 | + | "github.com/mum4k/termdash/widgets/textinput" |
| 35 | + | ) |
| 36 | + | |
| 37 | + | // buttonChunks creates the text chunks for a button from the provided text. |
| 38 | + | func buttonChunks(text string) []*button.TextChunk { |
| 39 | + | if len(text) == 0 { |
| 40 | + | return nil |
| 41 | + | } |
| 42 | + | first := string(text[0]) |
| 43 | + | rest := string(text[1:]) |
| 44 | + | |
| 45 | + | return []*button.TextChunk{ |
| 46 | + | button.NewChunk( |
| 47 | + | "<", |
| 48 | + | button.TextCellOpts(cell.FgColor(cell.ColorWhite)), |
| 49 | + | button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)), |
| 50 | + | button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)), |
| 51 | + | ), |
| 52 | + | button.NewChunk( |
| 53 | + | first, |
| 54 | + | button.TextCellOpts(cell.FgColor(cell.ColorRed)), |
| 55 | + | ), |
| 56 | + | button.NewChunk( |
| 57 | + | rest, |
| 58 | + | button.TextCellOpts(cell.FgColor(cell.ColorWhite)), |
| 59 | + | button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)), |
| 60 | + | button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)), |
| 61 | + | ), |
| 62 | + | button.NewChunk( |
| 63 | + | ">", |
| 64 | + | button.TextCellOpts(cell.FgColor(cell.ColorWhite)), |
| 65 | + | button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)), |
| 66 | + | button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)), |
| 67 | + | ), |
| 68 | + | } |
| 69 | + | } |
| 70 | + | |
| 71 | + | // form contains the elements of a text input form. |
| 72 | + | type form struct { |
| 73 | + | // userInput is a text input that accepts user name. |
| 74 | + | userInput *textinput.TextInput |
| 75 | + | // uidInput is a text input that accepts UID. |
| 76 | + | uidInput *textinput.TextInput |
| 77 | + | // gidInput is a text input that accepts GID. |
| 78 | + | gidInput *textinput.TextInput |
| 79 | + | // homeInput is a text input that accepts path to the home folder. |
| 80 | + | homeInput *textinput.TextInput |
| 81 | + | |
| 82 | + | // submitB is a button that submits the form. |
| 83 | + | submitB *button.Button |
| 84 | + | // cancelB is a button that exist the application. |
| 85 | + | cancelB *button.Button |
| 86 | + | } |
| 87 | + | |
| 88 | + | // newForm returns a new form instance. |
| 89 | + | // The cancel argument is a function that terminates the application when called. |
| 90 | + | func newForm(cancel context.CancelFunc) (*form, error) { |
| 91 | + | var username string |
| 92 | + | u, err := user.Current() |
| 93 | + | if err != nil { |
| 94 | + | username = "mum4k" |
| 95 | + | } else { |
| 96 | + | username = u.Username |
| 97 | + | } |
| 98 | + | |
| 99 | + | userInput, err := textinput.New( |
| 100 | + | textinput.Label("Username: ", cell.FgColor(cell.ColorNumber(33))), |
| 101 | + | textinput.DefaultText(username), |
| 102 | + | textinput.MaxWidthCells(20), |
| 103 | + | textinput.ExclusiveKeyboardOnFocus(), |
| 104 | + | ) |
| 105 | + | uidInput, err := textinput.New( |
| 106 | + | textinput.Label("UID: ", cell.FgColor(cell.ColorNumber(33))), |
| 107 | + | textinput.DefaultText("1000"), |
| 108 | + | textinput.MaxWidthCells(20), |
| 109 | + | textinput.ExclusiveKeyboardOnFocus(), |
| 110 | + | ) |
| 111 | + | gidInput, err := textinput.New( |
| 112 | + | textinput.Label("GID: ", cell.FgColor(cell.ColorNumber(33))), |
| 113 | + | textinput.DefaultText("1000"), |
| 114 | + | textinput.MaxWidthCells(20), |
| 115 | + | textinput.ExclusiveKeyboardOnFocus(), |
| 116 | + | ) |
| 117 | + | homeInput, err := textinput.New( |
| 118 | + | textinput.Label("Home: ", cell.FgColor(cell.ColorNumber(33))), |
| 119 | + | textinput.DefaultText(fmt.Sprintf("/home/%s", username)), |
| 120 | + | textinput.MaxWidthCells(20), |
| 121 | + | textinput.ExclusiveKeyboardOnFocus(), |
| 122 | + | ) |
| 123 | + | |
| 124 | + | submitB, err := button.NewFromChunks(buttonChunks("Submit"), nil, |
| 125 | + | button.Key(keyboard.KeyEnter), |
| 126 | + | button.GlobalKeys('s', 'S'), |
| 127 | + | button.DisableShadow(), |
| 128 | + | button.Height(1), |
| 129 | + | button.TextHorizontalPadding(0), |
| 130 | + | button.FillColor(cell.ColorBlack), |
| 131 | + | button.FocusedFillColor(cell.ColorNumber(117)), |
| 132 | + | button.PressedFillColor(cell.ColorNumber(220)), |
| 133 | + | ) |
| 134 | + | if err != nil { |
| 135 | + | panic(err) |
| 136 | + | } |
| 137 | + | |
| 138 | + | cancelB, err := button.NewFromChunks(buttonChunks("Cancel"), func() error { |
| 139 | + | cancel() |
| 140 | + | return nil |
| 141 | + | }, |
| 142 | + | button.FillColor(cell.ColorNumber(220)), |
| 143 | + | button.Key(keyboard.KeyEnter), |
| 144 | + | button.GlobalKeys('c', 'C'), |
| 145 | + | button.DisableShadow(), |
| 146 | + | button.Height(1), |
| 147 | + | button.TextHorizontalPadding(0), |
| 148 | + | button.FillColor(cell.ColorBlack), |
| 149 | + | button.FocusedFillColor(cell.ColorNumber(117)), |
| 150 | + | button.PressedFillColor(cell.ColorNumber(220)), |
| 151 | + | ) |
| 152 | + | if err != nil { |
| 153 | + | panic(err) |
| 154 | + | } |
| 155 | + | |
| 156 | + | return &form{ |
| 157 | + | userInput: userInput, |
| 158 | + | uidInput: uidInput, |
| 159 | + | gidInput: gidInput, |
| 160 | + | homeInput: homeInput, |
| 161 | + | submitB: submitB, |
| 162 | + | cancelB: cancelB, |
| 163 | + | }, nil |
| 164 | + | } |
| 165 | + | |
| 166 | + | // formLayout updates the container into a layout with text inputs and buttons. |
| 167 | + | func formLayout(c *container.Container, f *form) error { |
| 168 | + | return c.Update("root", |
| 169 | + | container.KeyFocusNext(keyboard.KeyTab), |
| 170 | + | container.KeyFocusGroupsNext(keyboard.KeyArrowDown, 1), |
| 171 | + | container.KeyFocusGroupsPrevious(keyboard.KeyArrowUp, 1), |
| 172 | + | container.KeyFocusGroupsNext(keyboard.KeyArrowRight, 2), |
| 173 | + | container.KeyFocusGroupsPrevious(keyboard.KeyArrowLeft, 2), |
| 174 | + | container.SplitHorizontal( |
| 175 | + | container.Top( |
| 176 | + | container.Border(linestyle.Light), |
| 177 | + | container.SplitHorizontal( |
| 178 | + | container.Top( |
| 179 | + | container.SplitHorizontal( |
| 180 | + | container.Top( |
| 181 | + | container.Focused(), |
| 182 | + | container.KeyFocusGroups(1), |
| 183 | + | container.PlaceWidget(f.userInput), |
| 184 | + | ), |
| 185 | + | container.Bottom( |
| 186 | + | container.KeyFocusGroups(1), |
| 187 | + | container.KeyFocusSkip(), |
| 188 | + | container.PlaceWidget(f.uidInput), |
| 189 | + | ), |
| 190 | + | ), |
| 191 | + | ), |
| 192 | + | container.Bottom( |
| 193 | + | container.SplitHorizontal( |
| 194 | + | container.Top( |
| 195 | + | container.KeyFocusGroups(1), |
| 196 | + | container.KeyFocusSkip(), |
| 197 | + | container.PlaceWidget(f.gidInput), |
| 198 | + | ), |
| 199 | + | container.Bottom( |
| 200 | + | container.KeyFocusGroups(1), |
| 201 | + | container.KeyFocusSkip(), |
| 202 | + | container.PlaceWidget(f.homeInput), |
| 203 | + | ), |
| 204 | + | ), |
| 205 | + | ), |
| 206 | + | ), |
| 207 | + | ), |
| 208 | + | container.Bottom( |
| 209 | + | container.SplitHorizontal( |
| 210 | + | container.Top( |
| 211 | + | container.SplitVertical( |
| 212 | + | container.Left( |
| 213 | + | container.KeyFocusGroups(1, 2), |
| 214 | + | container.PlaceWidget(f.submitB), |
| 215 | + | container.AlignHorizontal(align.HorizontalRight), |
| 216 | + | container.PaddingRight(5), |
| 217 | + | ), |
| 218 | + | container.Right( |
| 219 | + | container.KeyFocusGroups(1, 2), |
| 220 | + | container.PlaceWidget(f.cancelB), |
| 221 | + | container.AlignHorizontal(align.HorizontalLeft), |
| 222 | + | container.PaddingLeft(5), |
| 223 | + | ), |
| 224 | + | ), |
| 225 | + | ), |
| 226 | + | container.Bottom( |
| 227 | + | container.KeyFocusSkip(), |
| 228 | + | ), |
| 229 | + | container.SplitFixed(3), |
| 230 | + | ), |
| 231 | + | ), |
| 232 | + | container.SplitFixed(6), |
| 233 | + | ), |
| 234 | + | ) |
| 235 | + | } |
| 236 | + | |
| 237 | + | // submitLayout updates the container into a layout that displays the submitted data. |
| 238 | + | // The cancel argument is a function that terminates Termdash when called. |
| 239 | + | func submitLayout(c *container.Container, f *form, cancel context.CancelFunc) error { |
| 240 | + | t, err := text.New() |
| 241 | + | if err != nil { |
| 242 | + | return err |
| 243 | + | } |
| 244 | + | |
| 245 | + | if err := t.Write("Submitted data:\n\n"); err != nil { |
| 246 | + | return err |
| 247 | + | } |
| 248 | + | if err := t.Write(fmt.Sprintf("Username: %s\n", f.userInput.Read())); err != nil { |
| 249 | + | return err |
| 250 | + | } |
| 251 | + | if err := t.Write(fmt.Sprintf("UID: %s\n", f.uidInput.Read())); err != nil { |
| 252 | + | return err |
| 253 | + | } |
| 254 | + | if err := t.Write(fmt.Sprintf("GID: %s\n", f.gidInput.Read())); err != nil { |
| 255 | + | return err |
| 256 | + | } |
| 257 | + | if err := t.Write(fmt.Sprintf("Home: %s\n", f.homeInput.Read())); err != nil { |
| 258 | + | return err |
| 259 | + | } |
| 260 | + | |
| 261 | + | okB, err := button.NewFromChunks(buttonChunks("OK"), func() error { |
| 262 | + | cancel() |
| 263 | + | return nil |
| 264 | + | }, |
| 265 | + | button.FillColor(cell.ColorNumber(220)), |
| 266 | + | button.Key(keyboard.KeyEnter), |
| 267 | + | button.GlobalKeys('o', 'O'), |
| 268 | + | button.DisableShadow(), |
| 269 | + | button.Height(1), |
| 270 | + | button.TextHorizontalPadding(0), |
| 271 | + | button.FillColor(cell.ColorBlack), |
| 272 | + | button.FocusedFillColor(cell.ColorNumber(117)), |
| 273 | + | button.PressedFillColor(cell.ColorNumber(220)), |
| 274 | + | ) |
| 275 | + | if err != nil { |
| 276 | + | return err |
| 277 | + | } |
| 278 | + | |
| 279 | + | return c.Update("root", |
| 280 | + | container.SplitHorizontal( |
| 281 | + | container.Top( |
| 282 | + | container.SplitVertical( |
| 283 | + | container.Left(), |
| 284 | + | container.Right( |
| 285 | + | container.PlaceWidget(t), |
| 286 | + | ), |
| 287 | + | container.SplitPercent(33), |
| 288 | + | ), |
| 289 | + | ), |
| 290 | + | container.Bottom( |
| 291 | + | container.Focused(), |
| 292 | + | container.PlaceWidget(okB), |
| 293 | + | ), |
| 294 | + | container.SplitFixed(7), |
| 295 | + | ), |
| 296 | + | ) |
| 297 | + | } |
| 298 | + | |
| 299 | + | func main() { |
| 300 | + | t, err := tcell.New() |
| 301 | + | if err != nil { |
| 302 | + | panic(err) |
| 303 | + | } |
| 304 | + | defer t.Close() |
| 305 | + | |
| 306 | + | ctx, cancel := context.WithCancel(context.Background()) |
| 307 | + | c, err := container.New(t, container.ID("root")) |
| 308 | + | if err != nil { |
| 309 | + | panic(err) |
| 310 | + | } |
| 311 | + | |
| 312 | + | f, err := newForm(cancel) |
| 313 | + | if err != nil { |
| 314 | + | panic(err) |
| 315 | + | } |
| 316 | + | f.submitB.SetCallback(func() error { |
| 317 | + | return submitLayout(c, f, cancel) |
| 318 | + | }) |
| 319 | + | if err := formLayout(c, f); err != nil { |
| 320 | + | panic(err) |
| 321 | + | } |
| 322 | + | |
| 323 | + | if err := termdash.Run(ctx, t, c, termdash.RedrawInterval(100*time.Millisecond)); err != nil { |
| 324 | + | panic(err) |
| 325 | + | } |
| 326 | + | } |
| 327 | + | |