background code checker for golang

model: introduce config

+337 -131
+272
config.go
··· 1 + package gust 2 + 3 + import ( 4 + "bytes" 5 + "fmt" 6 + "os" 7 + "strings" 8 + 9 + "github.com/BurntSushi/toml" 10 + "github.com/charmbracelet/lipgloss" 11 + ) 12 + 13 + type Config struct { 14 + Mode string `toml:"mode"` 15 + Package string `toml:"package"` 16 + Summarized bool `toml:"summarized"` 17 + Help bool `toml:"help"` 18 + Styles Styles `toml:"styles"` 19 + Signs Signs `toml:"signs"` 20 + } 21 + 22 + type Styles struct { 23 + Mode Style `toml:"mode"` 24 + Success Style `toml:"success"` 25 + Running Style `toml:"running"` 26 + Error Style `toml:"error"` 27 + Warning Style `toml:"warning"` 28 + Duration Style `toml:"duration"` 29 + Separator Style `toml:"separator"` 30 + File Style `toml:"file"` 31 + Line Style `toml:"line"` 32 + Context ContextStyles `toml:"context"` 33 + } 34 + 35 + type ContextStyles struct { 36 + ActiveLine Style `toml:"activeLine"` 37 + ActiveLineNr Style `toml:"activeLineNr"` 38 + PassiveLine Style `toml:"passiveLine"` 39 + PassiveLineNr Style `toml:"passiveLineNr"` 40 + } 41 + 42 + type Signs struct { 43 + Error string `toml:"error"` 44 + Warning string `toml:"warning"` 45 + System string `toml:"system"` 46 + Separator string `toml:"separator"` 47 + PassiveIndent string `toml:"passiveIndent"` 48 + ActiveIndent string `toml:"activeIndent"` 49 + } 50 + 51 + type Style struct { 52 + lipgloss.Style 53 + } 54 + 55 + type styleTOML struct { 56 + Fg string `toml:"fg,omitempty"` 57 + Bg string `toml:"bg,omitempty"` 58 + Bold bool `toml:"bold,omitempty"` 59 + Italic bool `toml:"italic,omitempty"` 60 + Underline bool `toml:"underline,omitempty"` 61 + Faint bool `toml:"faint,omitempty"` 62 + Reverse bool `toml:"reverse,omitempty"` 63 + } 64 + 65 + func (s Style) MarshalTOML() ([]byte, error) { 66 + fg := "5" 67 + bg := "" 68 + 69 + color, err := marshalColor(s.GetForeground()) 70 + if err != nil { 71 + return nil, err 72 + } 73 + if color != nil { 74 + fg = *color 75 + } 76 + 77 + color, err = marshalColor(s.GetBackground()) 78 + if err != nil { 79 + return nil, err 80 + } 81 + if color != nil { 82 + bg = *color 83 + } 84 + 85 + tomlData := styleTOML{ 86 + Fg: fg, 87 + Bg: bg, 88 + Bold: s.GetBold(), 89 + Italic: s.GetItalic(), 90 + Underline: s.GetUnderline(), 91 + Faint: s.GetFaint(), 92 + Reverse: s.GetReverse(), 93 + } 94 + 95 + var buf bytes.Buffer 96 + err = toml.NewEncoder(&buf).Encode(tomlData) 97 + if err != nil { 98 + return nil, fmt.Errorf("failed to encode styleTOML to TOML: %w", err) 99 + } 100 + 101 + return buf.Bytes(), nil 102 + } 103 + 104 + func (s styleTOML) MarshalTOML() ([]byte, error) { 105 + var parts []string 106 + 107 + if s.Fg != "" { 108 + parts = append(parts, fmt.Sprintf("fg = \"%s\"", s.Fg)) 109 + } 110 + if s.Bg != "" { 111 + parts = append(parts, fmt.Sprintf("bg = \"%s\"", s.Bg)) 112 + } 113 + if s.Bold { 114 + parts = append(parts, fmt.Sprintf("bold = %v", s.Bold)) 115 + } 116 + if s.Italic { 117 + parts = append(parts, fmt.Sprintf("italic = %v", s.Italic)) 118 + } 119 + if s.Underline { 120 + parts = append(parts, fmt.Sprintf("underline = %v", s.Underline)) 121 + } 122 + if s.Faint { 123 + parts = append(parts, fmt.Sprintf("faint = %v", s.Faint)) 124 + } 125 + if s.Reverse { 126 + parts = append(parts, fmt.Sprintf("reverse = %v", s.Reverse)) 127 + } 128 + 129 + if len(parts) == 0 { 130 + return nil, nil 131 + } 132 + 133 + return []byte("{ " + strings.Join(parts, ", ") + " }"), nil 134 + } 135 + 136 + func marshalColor(tc lipgloss.TerminalColor) (*string, error) { 137 + if tc != nil { 138 + switch color := tc.(type) { 139 + case lipgloss.ANSIColor: 140 + s := fmt.Sprintf("%d", color) 141 + return &s, nil 142 + case lipgloss.Color: 143 + s := string(color) 144 + return &s, nil 145 + case lipgloss.NoColor: 146 + return nil, nil 147 + default: 148 + return nil, fmt.Errorf("fg is not serializable, type: %T", color) 149 + } 150 + } else { 151 + return nil, nil 152 + } 153 + } 154 + 155 + func (s *Style) UnmarshalTOML(data any) error { 156 + m := data.(map[string]any) 157 + style := lipgloss.NewStyle() 158 + 159 + if v, ok := m["fg"].(string); ok { 160 + style = style.Foreground(lipgloss.Color(v)) 161 + } 162 + if v, ok := m["bg"].(string); ok { 163 + style = style.Background(lipgloss.Color(v)) 164 + } 165 + if v, ok := m["bold"].(bool); ok { 166 + style = style.Bold(v) 167 + } 168 + if v, ok := m["italic"].(bool); ok { 169 + style = style.Italic(v) 170 + } 171 + if v, ok := m["underline"].(bool); ok { 172 + style = style.Underline(v) 173 + } 174 + if v, ok := m["faint"].(bool); ok { 175 + style = style.Faint(v) 176 + } 177 + if v, ok := m["reverse"].(bool); ok { 178 + style = style.Reverse(v) 179 + } 180 + s.Style = style 181 + return nil 182 + } 183 + 184 + func (s *Style) UnmarshalText(text []byte) error { 185 + str := string(text) 186 + tokens := strings.Fields(str) 187 + style := lipgloss.NewStyle() 188 + for i := 0; i < len(tokens); i++ { 189 + switch tokens[i] { 190 + case "bold": 191 + style = style.Bold(true) 192 + case "italic": 193 + style = style.Italic(true) 194 + case "underline": 195 + style = style.Underline(true) 196 + case "faint": 197 + style = style.Faint(true) 198 + case "on": 199 + if i+1 < len(tokens) { 200 + style = style.Background(lipgloss.Color(tokens[i+1])) 201 + i++ 202 + } 203 + default: 204 + style = style.Foreground(lipgloss.Color(tokens[i])) 205 + } 206 + } 207 + s.Style = style 208 + return nil 209 + } 210 + 211 + func DefaultConfig() Config { 212 + return Config{ 213 + Summarized: false, 214 + Help: true, 215 + Mode: "build", 216 + Package: "./cmd/gust", 217 + Styles: Styles{ 218 + Mode: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("5"))}, 219 + Success: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("2"))}, 220 + Running: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))}, 221 + Error: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))}, 222 + Warning: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))}, 223 + File: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("4"))}, 224 + Duration: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))}, 225 + Separator: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("8"))}, 226 + Line: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))}, 227 + Context: ContextStyles{ 228 + ActiveLine: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))}, 229 + ActiveLineNr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))}, 230 + PassiveLine: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))}, 231 + PassiveLineNr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))}, 232 + }, 233 + }, 234 + Signs: Signs{ 235 + Error: "err", 236 + Warning: "wrn", 237 + System: "sys", 238 + Separator: " · ", 239 + PassiveIndent: " | ", 240 + ActiveIndent: " > ", 241 + }, 242 + } 243 + } 244 + 245 + func (cfg Config) Dump() { 246 + err := toml.NewEncoder(os.Stdout).Encode(cfg) 247 + if err != nil { 248 + fmt.Fprintf(os.Stderr, "Failed to write config: %v", err) 249 + } 250 + } 251 + 252 + func LoadConfig() Config { 253 + cfg := DefaultConfig() 254 + 255 + if _, err := toml.DecodeFile("config.toml", &cfg); err != nil { 256 + fmt.Fprintf(os.Stderr, "%s", err.Error()) 257 + } 258 + 259 + return cfg 260 + } 261 + 262 + func WithPackage(pkg string) func(*Config) { 263 + return func(cfg *Config) { 264 + cfg.Package = pkg 265 + } 266 + } 267 + 268 + func WithMode(mode string) func(*Config) { 269 + return func(cfg *Config) { 270 + cfg.Mode = mode 271 + } 272 + }
+1
go.mod
··· 11 11 ) 12 12 13 13 require ( 14 + github.com/BurntSushi/toml v1.5.0 // indirect 14 15 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 15 16 github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 16 17 github.com/charmbracelet/x/ansi v0.8.0 // indirect
+2
go.sum
··· 1 + github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 + github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 1 3 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 2 4 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 3 5 github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
+15 -84
model.go
··· 8 8 9 9 "github.com/charmbracelet/bubbles/spinner" 10 10 tea "github.com/charmbracelet/bubbletea" 11 - "github.com/charmbracelet/lipgloss" 12 11 "github.com/fsnotify/fsnotify" 13 12 ) 14 13 ··· 17 16 stderr string 18 17 stdout string 19 18 20 - mode string 21 19 messages []CompilerMessage 22 20 fs Fs 23 21 w *fsnotify.Watcher 24 - options Options 22 + config Config 25 23 context int 26 24 27 25 status Status ··· 31 29 width, height int 32 30 } 33 31 34 - type Options struct { 35 - summarized bool 36 - help bool 37 - styles Styles 38 - signs Signs 39 - } 40 - 41 - type Styles struct { 42 - mode lipgloss.Style 43 - success lipgloss.Style 44 - running lipgloss.Style 45 - error lipgloss.Style 46 - warning lipgloss.Style 47 - duration lipgloss.Style 48 - separator lipgloss.Style 49 - file lipgloss.Style 50 - line lipgloss.Style 51 - context ContextStyles 52 - } 53 - 54 - type ContextStyles struct { 55 - activeLine lipgloss.Style 56 - activeLineNr lipgloss.Style 57 - passiveLine lipgloss.Style 58 - passiveLineNr lipgloss.Style 59 - } 32 + func NewModel(watcher *fsnotify.Watcher, config Config, options ...func(*Config)) Model { 33 + for _, o := range options { 34 + o(&config) 35 + } 60 36 61 - type Signs struct { 62 - // sign for error messages 63 - error string 64 - // sign for warning messages 65 - warning string 66 - // sign for system messages 67 - system string 68 - // separator for UI bits 69 - separator string 70 - // indentation prefix string for passive line in context 71 - passiveIndent string 72 - // indentation prefix string for active line in context 73 - activeIndent string 74 - } 75 - 76 - func NewModel(watcher *fsnotify.Watcher) Model { 77 37 return Model{ 78 38 w: watcher, 79 - mode: "build", 80 39 status: NewStatus(), 81 40 context: 1, 82 - options: Options{ 83 - summarized: false, 84 - help: true, 85 - styles: Styles{ 86 - mode: lipgloss.NewStyle().Foreground(lipgloss.Color("5")), 87 - success: lipgloss.NewStyle().Foreground(lipgloss.Color("2")), 88 - running: lipgloss.NewStyle().Foreground(lipgloss.Color("3")), 89 - error: lipgloss.NewStyle().Foreground(lipgloss.Color("1")), 90 - warning: lipgloss.NewStyle().Foreground(lipgloss.Color("3")), 91 - file: lipgloss.NewStyle().Foreground(lipgloss.Color("4")), 92 - duration: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), 93 - separator: lipgloss.NewStyle().Foreground(lipgloss.Color("8")), 94 - line: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), 95 - context: ContextStyles{ 96 - activeLine: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), 97 - activeLineNr: lipgloss.NewStyle().Foreground(lipgloss.Color("1")), 98 - passiveLine: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), 99 - passiveLineNr: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), 100 - }, 101 - }, 102 - signs: Signs{ 103 - error: "err", 104 - warning: "wrn", 105 - system: "sys", 106 - separator: " · ", 107 - passiveIndent: " | ", 108 - activeIndent: " > ", 109 - }, 110 - }, 41 + config: config, 111 42 } 112 43 } 113 44 ··· 126 57 return m, tea.Quit 127 58 case "r": 128 59 var cmd tea.Cmd 129 - if m.mode != "run" { 130 - m.mode = "run" 60 + if m.config.Mode != "run" { 61 + m.config.Mode = "run" 131 62 cmd = reload 132 63 } 133 64 return m, cmd 134 65 case "b": 135 66 var cmd tea.Cmd 136 - if m.mode != "build" { 137 - m.mode = "build" 67 + if m.config.Mode != "build" { 68 + m.config.Mode = "build" 138 69 cmd = reload 139 70 } 140 71 return m, cmd 141 72 case "s": 142 - m.options.summarized = !m.options.summarized 73 + m.config.Summarized = !m.config.Summarized 143 74 return m, nil 144 75 case "h", "?": 145 - m.options.help = !m.options.help 76 + m.config.Help = !m.config.Help 146 77 return m, nil 147 78 default: 148 79 return m, nil ··· 196 127 cmd := Success.Cmd() 197 128 m.systemErrors = []error{} 198 129 199 - command := exec.Command("go", m.mode, "./cmd/gust") 130 + command := exec.Command("go", m.config.Mode, m.config.Package) 200 131 stderr, err := command.StderrPipe() 201 132 if err != nil { 202 133 m.systemErrors = append(m.systemErrors, err) ··· 229 160 m.stderr = string(stderrData) 230 161 m.stdout = string(stdoutData) 231 162 232 - if m.mode == "build" { 163 + if m.config.Mode == "build" { 233 164 messages := Parse(m.stderr) 234 165 fs := Fs{} 235 - if !m.options.summarized { 166 + if !m.config.Summarized { 236 167 fs = BuildFs(messages) 237 168 fs.PopulateContext(messages, m.context) 238 169 }
+47 -47
view.go
··· 28 28 } 29 29 } 30 30 31 - style := m.options.styles 32 - sign := m.options.signs 31 + style := m.config.Styles 32 + sign := m.config.Signs 33 33 34 34 for _, msg := range m.messages { 35 35 var mb strings.Builder 36 36 if msg.Type == "error" { 37 - mb.WriteString(style.error.Render(sign.error)) 37 + mb.WriteString(style.Error.Render(sign.Error)) 38 38 mb.WriteString(" ") 39 39 } else { 40 - mb.WriteString(style.warning.Render(sign.warning)) 40 + mb.WriteString(style.Warning.Render(sign.Warning)) 41 41 mb.WriteString(" ") 42 42 } 43 43 44 44 if msg.Location.Line != "" { 45 - mb.WriteString(style.line.Align(lipgloss.Right).Width(maxLineLen).Render(msg.Location.Line)) 45 + mb.WriteString(style.Line.Align(lipgloss.Right).Width(maxLineLen).Render(msg.Location.Line)) 46 46 47 47 if msg.Location.Column != "" { 48 48 mb.WriteString(":") 49 - mb.WriteString(style.line.Align(lipgloss.Left).Width(maxColLen).Render(msg.Location.Column)) 49 + mb.WriteString(style.Line.Align(lipgloss.Left).Width(maxColLen).Render(msg.Location.Column)) 50 50 } 51 51 } 52 52 if msg.Location.File != "" { 53 53 mb.WriteString(" ") 54 - mb.WriteString(style.file.Render(msg.Location.File)) 54 + mb.WriteString(style.File.Render(msg.Location.File)) 55 55 mb.WriteString(" ") 56 56 } 57 57 ··· 60 60 b.WriteString(wordwrap.String(mb.String(), m.width)) 61 61 62 62 // display context 63 - if !m.options.summarized { 63 + if !m.config.Summarized { 64 64 b.WriteString("\n") 65 65 start := msg.Context.Start() 66 - style := style.context 66 + style := style.Context 67 67 for offset, line := range msg.Context.Ordered() { 68 68 nr := fmt.Sprintf("%d", start+offset) 69 - nrStyle := style.passiveLineNr 70 - lineStyle := style.passiveLine 71 - indent := sign.passiveIndent 69 + nrStyle := style.PassiveLineNr 70 + lineStyle := style.PassiveLine 71 + indent := sign.PassiveIndent 72 72 active := false 73 73 if msg.Location.Line == nr { 74 74 active = true 75 - nrStyle = style.activeLineNr 76 - lineStyle = style.activeLine 77 - indent = sign.activeIndent 75 + nrStyle = style.ActiveLineNr 76 + lineStyle = style.ActiveLine 77 + indent = sign.ActiveIndent 78 78 } 79 79 b.WriteString(nrStyle.Render(indent)) 80 80 b.WriteString(nrStyle.Align(lipgloss.Right).Render(nr)) ··· 88 88 tabCount := strings.Count(line[:col-1], "\t") 89 89 90 90 b.WriteString("\n") 91 - b.WriteString(style.passiveLineNr.Render(sign.passiveIndent)) 91 + b.WriteString(style.PassiveLineNr.Render(sign.PassiveIndent)) 92 92 93 93 leftPadding := col + // current column to place the pointer at 94 94 3*tabCount + // if the line has tabs, we convert to spaces and offset 95 95 len(msg.Location.Line) + // we always print line-numbers, so offset by that too 96 96 1 // there is a single space between line-number and line content 97 - b.WriteString(style.activeLineNr.Width(leftPadding).Align(lipgloss.Right).Render("^")) 97 + b.WriteString(style.ActiveLineNr.Width(leftPadding).Align(lipgloss.Right).Render("^")) 98 98 } 99 99 b.WriteString("\n") 100 100 } ··· 104 104 } 105 105 106 106 for _, e := range m.systemErrors { 107 - b.WriteString(style.error.Render(sign.system)) 107 + b.WriteString(style.Error.Render(sign.System)) 108 108 b.WriteString(" ") 109 109 b.WriteString(e.Error()) 110 110 b.WriteString("\n") ··· 112 112 113 113 b.WriteString("\n") 114 114 115 - if m.options.help { 115 + if m.config.Help { 116 116 b.WriteString(m.viewHelp()) 117 117 } 118 118 ··· 132 132 } 133 133 } 134 134 135 - style := m.options.styles 136 - sign := m.options.signs 135 + style := m.config.Styles 136 + sign := m.config.Signs 137 137 138 138 // write mode 139 - b.WriteString(style.mode.Render(m.mode)) 139 + b.WriteString(style.Mode.Render(m.config.Mode)) 140 140 141 141 // write status 142 - statusStyle := style.success 142 + statusStyle := style.Success 143 143 switch m.status.Kind { 144 144 case Running, Initializing: 145 - statusStyle = style.running 145 + statusStyle = style.Running 146 146 case Error: 147 - statusStyle = style.error 147 + statusStyle = style.Error 148 148 } 149 149 if len(m.messages) == 0 { 150 - b.WriteString(style.separator.Render(sign.separator)) 150 + b.WriteString(style.Separator.Render(sign.Separator)) 151 151 b.WriteString(statusStyle.Render(m.status.View())) 152 152 } 153 153 ··· 161 161 162 162 // write message summary 163 163 if errorCount != 0 { 164 - b.WriteString(style.separator.Render(sign.separator)) 165 - b.WriteString(style.error.Render(fmt.Sprintf("%d error%s", errorCount, plural(errorCount)))) 164 + b.WriteString(style.Separator.Render(sign.Separator)) 165 + b.WriteString(style.Error.Render(fmt.Sprintf("%d error%s", errorCount, plural(errorCount)))) 166 166 } 167 167 if warningCount != 0 { 168 - b.WriteString(style.separator.Render(sign.separator)) 169 - b.WriteString(style.warning.Render(fmt.Sprintf("%d warning%s", warningCount, plural(warningCount)))) 168 + b.WriteString(style.Separator.Render(sign.Separator)) 169 + b.WriteString(style.Warning.Render(fmt.Sprintf("%d warning%s", warningCount, plural(warningCount)))) 170 170 } 171 171 172 - b.WriteString(style.separator.Render(sign.separator)) 173 - b.WriteString(style.duration.Render(m.duration.String())) 172 + b.WriteString(style.Separator.Render(sign.Separator)) 173 + b.WriteString(style.Duration.Render(m.duration.String())) 174 174 175 175 b.WriteString("\n") 176 176 ··· 179 179 180 180 func (m Model) viewHelp() string { 181 181 var b strings.Builder 182 - style := m.options.styles 183 - sign := m.options.signs 182 + style := m.config.Styles 183 + sign := m.config.Signs 184 184 185 185 b.WriteString("s ") 186 - if m.options.summarized { 187 - b.WriteString(style.separator.Render("show context")) 186 + if m.config.Summarized { 187 + b.WriteString(style.Separator.Render("show context")) 188 188 } else { 189 - b.WriteString(style.separator.Render("hide context")) 189 + b.WriteString(style.Separator.Render("hide context")) 190 190 } 191 - b.WriteString(style.separator.Render(sign.separator)) 191 + b.WriteString(style.Separator.Render(sign.Separator)) 192 192 193 - if m.mode != "build" { 193 + if m.config.Mode != "build" { 194 194 b.WriteString("b ") 195 - b.WriteString(style.separator.Render("build")) 196 - b.WriteString(style.separator.Render(sign.separator)) 195 + b.WriteString(style.Separator.Render("build")) 196 + b.WriteString(style.Separator.Render(sign.Separator)) 197 197 } 198 198 199 - if m.mode != "run" { 199 + if m.config.Mode != "run" { 200 200 b.WriteString("r ") 201 - b.WriteString(style.separator.Render("run")) 202 - b.WriteString(style.separator.Render(sign.separator)) 201 + b.WriteString(style.Separator.Render("run")) 202 + b.WriteString(style.Separator.Render(sign.Separator)) 203 203 } 204 204 205 205 b.WriteString("q ") 206 - b.WriteString(style.separator.Render("quit")) 207 - b.WriteString(style.separator.Render(sign.separator)) 206 + b.WriteString(style.Separator.Render("quit")) 207 + b.WriteString(style.Separator.Render(sign.Separator)) 208 208 209 209 b.WriteString("h/? ") 210 - b.WriteString(style.separator.Render("hide help")) 210 + b.WriteString(style.Separator.Render("hide help")) 211 211 212 212 return lipgloss.NewStyle().PaddingTop(2).Render(b.String()) 213 213 }