github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/config/remoteconfig/messages.go (about) 1 package remoteconfig 2 3 import ( 4 "math/rand" 5 "strings" 6 "time" 7 8 "github.com/ddev/ddev/pkg/config/remoteconfig/internal" 9 "github.com/ddev/ddev/pkg/config/remoteconfig/types" 10 "github.com/ddev/ddev/pkg/dockerutil" 11 "github.com/ddev/ddev/pkg/nodeps" 12 "github.com/ddev/ddev/pkg/output" 13 "github.com/ddev/ddev/pkg/styles" 14 "github.com/ddev/ddev/pkg/util" 15 "github.com/ddev/ddev/pkg/versionconstants" 16 "github.com/jedib0t/go-pretty/v6/table" 17 "github.com/jedib0t/go-pretty/v6/text" 18 ) 19 20 type messageTypes struct { 21 messageType types.MessageType 22 messages []internal.Message 23 } 24 25 type conditionDefinition struct { 26 name string 27 description string 28 conditionFunc func() bool 29 } 30 31 var conditionDefinitions = map[string]conditionDefinition{} 32 33 func init() { 34 AddCondition("Disabled", "Permanently disables the message", func() bool { return false }) 35 AddCondition("Colima", "Running on Colima", dockerutil.IsColima) 36 AddCondition("Lima", "Running on Lima", dockerutil.IsLima) 37 AddCondition("DockerDesktop", "Running on Docker Desktop", dockerutil.IsDockerDesktop) 38 AddCondition("WSL2", "Running on WSL2", nodeps.IsWSL2) 39 } 40 41 func AddCondition(name, description string, conditionFunc func() bool) { 42 conditionDefinitions[strings.ToLower(name)] = conditionDefinition{ 43 name: name, 44 description: description, 45 conditionFunc: conditionFunc, 46 } 47 } 48 49 func ListConditions() (conditions map[string]string) { 50 conditions = make(map[string]string) 51 52 for _, condition := range conditionDefinitions { 53 conditions[condition.name] = condition.description 54 } 55 56 return 57 } 58 59 // ShowNotifications shows notifications provided by the remote config to the user. 60 func (c *remoteConfig) ShowNotifications() { 61 // defer util.TimeTrack()() 62 63 if !c.showNotifications() { 64 return 65 } 66 67 for _, messages := range []messageTypes{ 68 {messageType: types.Info, messages: c.remoteConfig.Messages.Notifications.Infos}, 69 {messageType: types.Warning, messages: c.remoteConfig.Messages.Notifications.Warnings}, 70 } { 71 t := table.NewWriter() 72 73 var title string 74 var i int 75 76 switch messages.messageType { 77 case types.Warning: 78 applyTableStyle(warning, t) 79 title = "Important Warning" 80 default: 81 applyTableStyle(information, t) 82 title = "Important Message" 83 } 84 85 for _, message := range messages.messages { 86 if !c.checkConditions(message.Conditions) || !c.checkVersions(message.Versions) { 87 continue 88 } 89 90 t.AppendRow(table.Row{message.Message}) 91 i++ 92 } 93 94 if i == 0 { 95 continue 96 } 97 98 if i > 1 { 99 title += "s" 100 } 101 102 t.AppendHeader(table.Row{title}) 103 104 output.UserOut.Print("\n", t.Render(), "\n") 105 } 106 107 c.state.LastNotificationAt = time.Now() 108 if err := c.state.save(); err != nil { 109 util.Debug("Error while saving state: %s", err) 110 } 111 } 112 113 // ShowTicker shows ticker messages provided by the remote config to the user. 114 func (c *remoteConfig) ShowTicker() { 115 // defer util.TimeTrack()() 116 117 if !c.showTickerMessage() || len(c.remoteConfig.Messages.Ticker.Messages) == 0 { 118 return 119 } 120 121 messageOffset := c.state.LastTickerMessage 122 messageCount := len(c.remoteConfig.Messages.Ticker.Messages) 123 124 if messageOffset == 0 { 125 // As long as no message was shown, start with a random message. This 126 // is important for short living instances e.g. Gitpod to not always 127 // show the first message. A number from 0 to number of messages minus 128 // 1 is generated. 129 messageOffset = rand.Intn(messageCount) 130 } 131 132 for i := range c.remoteConfig.Messages.Ticker.Messages { 133 messageOffset++ 134 if messageOffset > messageCount { 135 messageOffset = 1 136 } 137 138 message := &c.remoteConfig.Messages.Ticker.Messages[i+messageOffset-1] 139 140 if c.checkConditions(message.Conditions) && c.checkVersions(message.Versions) { 141 t := table.NewWriter() 142 applyTableStyle(ticker, t) 143 144 var title string 145 146 if message.Title != "" { 147 title = message.Title 148 } else { 149 title = "Tip of the day" 150 } 151 152 t.AppendHeader(table.Row{title}) 153 t.AppendRow(table.Row{message.Message}) 154 155 output.UserOut.Print("\n", t.Render(), "\n") 156 157 c.state.LastTickerMessage = messageOffset 158 c.state.LastTickerAt = time.Now() 159 if err := c.state.save(); err != nil { 160 util.Debug("Error while saving state: %s", err) 161 } 162 163 break 164 } 165 } 166 } 167 168 // isNotificationsDisabled returns true if notifications should not be shown to 169 // the user which can be achieved by setting the related remote config. 170 func (c *remoteConfig) isNotificationsDisabled() bool { 171 return c.getNotificationsInterval() < 0 172 } 173 174 // getNotificationsInterval returns the notifications interval. The processing 175 // order is defined as follows, the first defined value is returned: 176 // - remote config 177 // - const notificationsInterval 178 func (c *remoteConfig) getNotificationsInterval() time.Duration { 179 if c.remoteConfig.Messages.Notifications.Interval != 0 { 180 return time.Duration(c.remoteConfig.Messages.Notifications.Interval) * time.Hour 181 } 182 183 return time.Duration(notificationsInterval) * time.Hour 184 } 185 186 // showNotifications returns true if notifications are not disabled and the 187 // notifications interval has been elapsed. 188 func (c *remoteConfig) showNotifications() bool { 189 return !output.JSONOutput && 190 !c.isNotificationsDisabled() && 191 c.state.LastNotificationAt.Add(c.getNotificationsInterval()).Before(time.Now()) 192 } 193 194 // isTickerDisabled returns true if tips should not be shown to the user which 195 // can be achieved by setting the related global config or also via the remote 196 // config. 197 func (c *remoteConfig) isTickerDisabled() bool { 198 return c.getTickerInterval() < 0 199 } 200 201 // getTickerInterval returns the ticker interval. The processing order is 202 // defined as follows, the first defined value is returned: 203 // - global config 204 // - remote config 205 // - const tickerInterval 206 func (c *remoteConfig) getTickerInterval() time.Duration { 207 if c.tickerInterval != 0 { 208 return time.Duration(c.tickerInterval) * time.Hour 209 } 210 211 if c.remoteConfig.Messages.Ticker.Interval != 0 { 212 return time.Duration(c.remoteConfig.Messages.Ticker.Interval) * time.Hour 213 } 214 215 return time.Duration(tickerInterval) * time.Hour 216 } 217 218 // showTickerMessage returns true if the ticker is not disabled and the ticker 219 // interval has been elapsed. 220 func (c *remoteConfig) showTickerMessage() bool { 221 return !output.JSONOutput && 222 !c.isTickerDisabled() && 223 c.state.LastTickerAt.Add(c.getTickerInterval()).Before(time.Now()) 224 } 225 226 func (c *remoteConfig) checkConditions(conditions []string) bool { 227 for _, rawCondition := range conditions { 228 condition, negated := strings.CutPrefix(strings.TrimSpace(rawCondition), "!") 229 condition = strings.ToLower(strings.TrimSpace(condition)) 230 231 conditionDef, found := conditionDefinitions[condition] 232 233 if found { 234 conditionResult := conditionDef.conditionFunc() 235 236 if (!negated && !conditionResult) || (negated && conditionResult) { 237 return false 238 } 239 } 240 } 241 242 return true 243 } 244 245 func (c *remoteConfig) checkVersions(versions string) bool { 246 versions = strings.TrimSpace(versions) 247 if versions != "" { 248 match, err := util.SemverValidate(versions, versionconstants.DdevVersion) 249 if err != nil { 250 util.Debug("Failed to validate DDEV version `%s` against constraint `%s`: %s", versionconstants.DdevVersion, versions, err) 251 return true 252 } 253 254 return match 255 } 256 257 return true 258 } 259 260 type preset int 261 262 const ( 263 information preset = iota 264 warning 265 ticker 266 ) 267 268 func applyTableStyle(preset preset, writer table.Writer) { 269 styles.SetGlobalTableStyle(writer) 270 271 termWidth, _ := nodeps.GetTerminalWidthHeight() 272 util.Debug("termWidth: %d", termWidth) 273 writer.SetColumnConfigs([]table.ColumnConfig{ 274 { 275 Number: 1, 276 WidthMin: 50, 277 WidthMax: int(termWidth) - 5, 278 WidthMaxEnforcer: text.WrapSoft, 279 }, 280 }) 281 282 style := writer.Style() 283 284 style.Options.SeparateRows = false 285 style.Options.SeparateFooter = false 286 style.Options.SeparateColumns = false 287 style.Options.SeparateHeader = false 288 style.Options.DrawBorder = false 289 290 switch preset { 291 case information: 292 style.Color = table.ColorOptions{ 293 Header: text.Colors{text.BgHiYellow, text.FgBlack}, 294 Row: text.Colors{text.BgHiYellow, text.FgBlack}, 295 } 296 case warning: 297 style.Color = table.ColorOptions{ 298 Header: text.Colors{text.BgHiRed, text.FgBlack}, 299 Row: text.Colors{text.BgHiRed, text.FgBlack}, 300 } 301 case ticker: 302 style.Color = table.ColorOptions{ 303 Header: text.Colors{text.BgHiWhite, text.FgBlack}, 304 Row: text.Colors{text.BgHiWhite, text.FgBlack}, 305 } 306 } 307 }