From 5fbeb457d5cff158f46264e5ad94b3e27b12c0f8 Mon Sep 17 00:00:00 2001 From: joce Date: Tue, 15 Feb 2022 20:36:45 -0500 Subject: [PATCH 01/11] Show stock lines in red when the change is < 0 --- filter.go | 2 +- layout.go | 8 ++++---- yahoo_quotes.go | 26 +++++++++++++++++++------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/filter.go b/filter.go index 68ae446..0cd77f5 100644 --- a/filter.go +++ b/filter.go @@ -63,7 +63,7 @@ func (filter *Filter) Apply(stocks []Stock) []Stock { values["avgVolume"] = stringToNumber(stock.AvgVolume) values["pe"] = stringToNumber(stock.PeRatio) values["peX"] = stringToNumber(stock.PeRatioX) - values["advancing"] = stock.Advancing // Remains bool. + values["direction"] = stock.Direction // Remains int. result, err := filter.profile.filterExpression.Evaluate(values) diff --git a/layout.go b/layout.go index e7549b4..4d88d2e 100644 --- a/layout.go +++ b/layout.go @@ -146,7 +146,7 @@ func (layout *Layout) prettify(quotes *Quotes) []Stock { // Iterate over the list of stocks and properly format all its columns. // for i, stock := range quotes.stocks { - pretty[i].Advancing = stock.Advancing + pretty[i].Direction = stock.Direction // // Iterate over the list of stock columns. For each column name: // - Get current column value. @@ -222,7 +222,7 @@ func buildQuotesTemplate() *template.Template { {{.Header}} -{{range.Stocks}}{{if .Advancing}}{{end}}{{.Ticker}}{{.LastTrade}}{{.Change}}{{.ChangePct}}{{.Open}}{{.Low}}{{.High}}{{.Low52}}{{.High52}}{{.Volume}}{{.AvgVolume}}{{.PeRatio}}{{.Dividend}}{{.Yield}}{{.MarketCap}}{{.PreOpen}}{{.AfterHours}} +{{range.Stocks}}{{if eq .Direction 1}}{{else if eq .Direction -1}}{{end}}{{.Ticker}}{{.LastTrade}}{{.Change}}{{.ChangePct}}{{.Open}}{{.Low}}{{.High}}{{.Low52}}{{.High52}}{{.Volume}}{{.AvgVolume}}{{.PeRatio}}{{.Dividend}}{{.Yield}}{{.MarketCap}}{{.PreOpen}}{{.AfterHours}} {{end}}` return template.Must(template.New(`quotes`).Parse(markup)) @@ -243,13 +243,13 @@ func group(stocks []Stock) []Stock { current := 0 for _, stock := range stocks { - if stock.Advancing { + if stock.Direction >= 0 { grouped[current] = stock current++ } } for _, stock := range stocks { - if !stock.Advancing { + if stock.Direction < 0 { grouped[current] = stock current++ } diff --git a/yahoo_quotes.go b/yahoo_quotes.go index c2523f6..d4fbe46 100644 --- a/yahoo_quotes.go +++ b/yahoo_quotes.go @@ -22,7 +22,7 @@ const quotesURLv7QueryParts = `&range=1d&interval=5m&indicators=close&includeTim const noDataIndicator = `N/A` // Stock stores quote information for the particular stock ticker. The data -// for all the fields except 'Advancing' is fetched using Yahoo market API. +// for all the fields except 'Direction' is fetched using Yahoo market API. type Stock struct { Ticker string `json:"symbol"` // Stock ticker. LastTrade string `json:"regularMarketPrice"` // l1: last trade. @@ -42,7 +42,7 @@ type Stock struct { MarketCap string `json:"marketCap"` // j3: market cap real time. MarketCapX string `json:"marketCap"` // j1: market cap (fallback when real time is N/A). Currency string `json:"currency"` // String code for currency of stock. - Advancing bool // True when change is >= $0. + Direction int // -1 when change is < $0, 0 when change is = $0, 1 when change is > $0. PreOpen string `json:"preMarketChangePercent,omitempty"` AfterHours string `json:"postMarketChangePercent,omitempty"` } @@ -188,8 +188,13 @@ func (quotes *Quotes) parse2(body []byte) (*Quotes, error) { fmt.Println("-------------------") */ adv, err := strconv.ParseFloat(quotes.stocks[i].Change, 64) + quotes.stocks[i].Direction = 0 if err == nil { - quotes.stocks[i].Advancing = adv >= 0.0 + if adv < 0.0 { + quotes.stocks[i].Direction = -1 + } else if adv > 0.0 { + quotes.stocks[i].Direction = 1 + } } } return quotes, nil @@ -202,7 +207,7 @@ func (quotes *Quotes) parse(body []byte) *Quotes { quotes.stocks = make([]Stock, len(lines)) // // Get the total number of fields in the Stock struct. Skip the last - // Advanicing field which is not fetched. + // Advancing field which is not fetched. // fieldsCount := reflect.ValueOf(quotes.stocks[0]).NumField() - 1 // @@ -226,10 +231,17 @@ func (quotes *Quotes) parse(body []byte) *Quotes { quotes.stocks[i].MarketCap = quotes.stocks[i].MarketCapX } // - // Stock is advancing if the change is not negative (i.e. $0.00 - // is also "advancing"). + // Get the direction of the stock // - quotes.stocks[i].Advancing = (quotes.stocks[i].Change[0:1] != `-`) + adv, err := strconv.ParseFloat(quotes.stocks[i].Change, 64) + quotes.stocks[i].Direction = 0 + if err == nil { + if adv < 0 { + quotes.stocks[i].Direction = -1 + } else if (adv > 0) { + quotes.stocks[i].Direction = 1 + } + } } return quotes From c8a69e9146a163108b1674abb0078f7f68eca671 Mon Sep 17 00:00:00 2001 From: joce Date: Tue, 15 Feb 2022 20:57:51 -0500 Subject: [PATCH 02/11] Show % in the header in red if < 0 --- cnn_market.go | 8 ++++---- layout.go | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/cnn_market.go b/cnn_market.go index f7d0648..06923d1 100644 --- a/cnn_market.go +++ b/cnn_market.go @@ -156,16 +156,16 @@ func (market *Market) extract(snippet []byte) *Market { market.Yield[`change`] = matches[11] market.Oil[`latest`] = matches[12] - market.Oil[`change`] = matches[13] + market.Oil[`change`] = matches[13] + `%` market.Yen[`latest`] = matches[14] - market.Yen[`change`] = matches[15] + market.Yen[`change`] = matches[15] + `%` market.Euro[`latest`] = matches[16] - market.Euro[`change`] = matches[17] + market.Euro[`change`] = matches[17] + `%` market.Gold[`latest`] = matches[18] - market.Gold[`change`] = matches[19] + market.Gold[`change`] = matches[19] + `%` market.Tokyo[`change`] = matches[20] market.Tokyo[`latest`] = matches[21] diff --git a/layout.go b/layout.go index 4d88d2e..b0da111 100644 --- a/layout.go +++ b/layout.go @@ -9,6 +9,7 @@ import ( "fmt" "reflect" "regexp" + "strconv" "strings" "text/template" "time" @@ -210,7 +211,7 @@ func (layout *Layout) pad(str string, width int) string { func buildMarketTemplate() *template.Template { markup := `Dow {{.Dow.change}} ({{.Dow.percent}}) at {{.Dow.latest}} S&P 500 {{.Sp500.change}} ({{.Sp500.percent}}) at {{.Sp500.latest}} NASDAQ {{.Nasdaq.change}} ({{.Nasdaq.percent}}) at {{.Nasdaq.latest}} Tokyo {{.Tokyo.change}} ({{.Tokyo.percent}}) at {{.Tokyo.latest}} HK {{.HongKong.change}} ({{.HongKong.percent}}) at {{.HongKong.latest}} London {{.London.change}} ({{.London.percent}}) at {{.London.latest}} Frankfurt {{.Frankfurt.change}} ({{.Frankfurt.percent}}) at {{.Frankfurt.latest}} {{if .IsClosed}}U.S. markets closed{{end}} -10-Year Yield {{.Yield.latest}}% ({{.Yield.change}}) Euro ${{.Euro.latest}} ({{.Euro.change}}%) Yen ¥{{.Yen.latest}} ({{.Yen.change}}%) Oil ${{.Oil.latest}} ({{.Oil.change}}%) Gold ${{.Gold.latest}} ({{.Gold.change}}%)` +10-Year Yield {{.Yield.latest}} ({{.Yield.change}}) Euro ${{.Euro.latest}} ({{.Euro.change}}) Yen ¥{{.Yen.latest}} ({{.Yen.change}}) Oil ${{.Oil.latest}} ({{.Oil.change}}) Gold ${{.Gold.latest}} ({{.Gold.change}})` return template.Must(template.New(`market`).Parse(markup)) } @@ -231,8 +232,17 @@ func buildQuotesTemplate() *template.Template { //----------------------------------------------------------------------------- func highlight(collections ...map[string]string) { for _, collection := range collections { - if collection[`change`][0:1] != `-` { - collection[`change`] = `` + collection[`change`] + `` + change := collection[`change`] + if change[len(change)-1:] == `%` { + change = change[0:len(change)-1] + } + adv, err := strconv.ParseFloat(change, 64) + if err == nil { + if adv < 0.0 { + collection[`change`] = `` + collection[`change`] + `` + } else if adv > 0.0 { + collection[`change`] = `` + collection[`change`] + `` + } } } } From dcedf2ad9ffdd1dc31287f1fd78f0d7a8fd086da Mon Sep 17 00:00:00 2001 From: joce Date: Tue, 15 Feb 2022 21:09:04 -0500 Subject: [PATCH 03/11] Yen value was not properly highlighted --- layout.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layout.go b/layout.go index b0da111..7f57ae4 100644 --- a/layout.go +++ b/layout.go @@ -83,7 +83,7 @@ func (layout *Layout) Market(market *Market) string { highlight(market.Dow, market.Sp500, market.Nasdaq, market.Tokyo, market.HongKong, market.London, market.Frankfurt, - market.Yield, market.Oil, market.Euro, market.Gold) + market.Yield, market.Oil, market.Euro, market.Yen, market.Gold) buffer := new(bytes.Buffer) layout.marketTemplate.Execute(buffer, market) From e3464aca1f039305419c7c79335b27b096cfe06f Mon Sep 17 00:00:00 2001 From: joce Date: Mon, 21 Feb 2022 22:20:09 -0500 Subject: [PATCH 04/11] Fix typos --- cnn_market.go | 2 +- markup.go | 2 +- screen.go | 4 ++-- sorter.go | 2 +- yahoo_quotes.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cnn_market.go b/cnn_market.go index 06923d1..40f1df9 100644 --- a/cnn_market.go +++ b/cnn_market.go @@ -108,7 +108,7 @@ func (market *Market) Fetch() (self *Market) { return market.extract(market.trim(body)) } -// Ok returns two values: 1) boolean indicating whether the error has occured, +// Ok returns two values: 1) boolean indicating whether the error has occurred, // and 2) the error text itself. func (market *Market) Ok() (bool, string) { return market.errors == ``, market.errors diff --git a/markup.go b/markup.go index d2abc5e..4bf5e64 100644 --- a/markup.go +++ b/markup.go @@ -77,7 +77,7 @@ func (markup *Markup) Tokenize(str string) []string { tail = match[0] if match[1] != 0 { if head != 0 || tail != 0 { - // Apend the text between tags. + // Append the text between tags. strings = append(strings, str[head:tail]) } // Append the tag itmarkup. diff --git a/screen.go b/screen.go index 51e8d8e..2c92614 100644 --- a/screen.go +++ b/screen.go @@ -12,8 +12,8 @@ import ( `fmt` ) -// Screen is thin wrapper aroung Termbox library to provide basic display -// capabilities as requied by Mop. +// Screen is thin wrapper around Termbox library to provide basic display +// capabilities as required by Mop. type Screen struct { width int // Current number of columns. height int // Current number of rows. diff --git a/sorter.go b/sorter.go index 7ea8e4a..d193557 100644 --- a/sorter.go +++ b/sorter.go @@ -223,7 +223,7 @@ func (sorter *Sorter) SortByCurrentColumn(stocks []Stock) *Sorter { } // The same exact method is used to sort by $Change and Change%. In both cases -// we sort by the value of Change% so that multiple $0.00s get sorted proferly. +// we sort by the value of Change% so that multiple $0.00s get sorted properly. func c(str string) float32 { c := "$" for _, v := range currencies { diff --git a/yahoo_quotes.go b/yahoo_quotes.go index d4fbe46..1807601 100644 --- a/yahoo_quotes.go +++ b/yahoo_quotes.go @@ -96,7 +96,7 @@ func (quotes *Quotes) Fetch() (self *Quotes) { return quotes } -// Ok returns two values: 1) boolean indicating whether the error has occured, +// Ok returns two values: 1) boolean indicating whether the error has occurred, // and 2) the error text itself. func (quotes *Quotes) Ok() (bool, string) { return quotes.errors == ``, quotes.errors From f20930d634cc8229eab429f1ea25b844c2270f9e Mon Sep 17 00:00:00 2001 From: joce Date: Mon, 21 Feb 2022 22:20:49 -0500 Subject: [PATCH 05/11] Read and save colors for gains, losses and tags --- profile.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/profile.go b/profile.go index ce393ed..29b8079 100644 --- a/profile.go +++ b/profile.go @@ -8,10 +8,15 @@ import ( "encoding/json" "io/ioutil" "sort" + "strings" "github.com/Knetic/govaluate" ) +const defaultGainColor = "green" +const defaultLossColor = "red" +const defaultTagColor = "yellow" + // Profile manages Mop program settings as defined by user (ex. list of // stock tickers). The settings are serialized using JSON and saved in // the ~/.moprc file. @@ -23,11 +28,32 @@ type Profile struct { Ascending bool // True when sort order is ascending. Grouped bool // True when stocks are grouped by advancing/declining. Filter string // Filter in human form + TickerColors struct { // Ticker colors + Gain string + Loss string + Tag string + } filterExpression *govaluate.EvaluableExpression // The filter as a govaluate expression selectedColumn int // Stores selected column number when the column editor is active. filename string // Path to the file in which the configuration is stored } +func IsSupportedColor(colorName string) bool { + switch colorName { + case + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white": + return true + } + return false +} + // Creates the profile and attempts to load the settings from ~/.moprc file. // If the file is not there it gets created with default values. func NewProfile(filename string) *Profile { @@ -41,9 +67,28 @@ func NewProfile(filename string) *Profile { profile.SortColumn = 0 // Stock quotes are sorted by ticker name. profile.Ascending = true // A to Z. profile.Filter = "" + profile.TickerColors.Gain = defaultGainColor + profile.TickerColors.Loss = defaultLossColor + profile.TickerColors.Tag = defaultTagColor profile.Save() } else { json.Unmarshal(data, profile) + + profile.TickerColors.Gain = strings.ToLower(profile.TickerColors.Gain) + if !IsSupportedColor(profile.TickerColors.Gain) { + profile.TickerColors.Gain = defaultGainColor + } + + profile.TickerColors.Loss = strings.ToLower(profile.TickerColors.Loss) + if !IsSupportedColor(profile.TickerColors.Loss) { + profile.TickerColors.Loss = defaultLossColor + } + + profile.TickerColors.Tag = strings.ToLower(profile.TickerColors.Tag) + if !IsSupportedColor(profile.TickerColors.Tag) { + profile.TickerColors.Tag = defaultTagColor + } + profile.SetFilter(profile.Filter) } profile.selectedColumn = -1 @@ -61,7 +106,7 @@ func (profile *Profile) Save() error { return ioutil.WriteFile(profile.filename, data, 0644) } -// AddTickers updates the list of existing tikers to add the new ones making +// AddTickers updates the list of existing tickers to add the new ones making // sure there are no duplicates. func (profile *Profile) AddTickers(tickers []string) (added int, err error) { added, err = 0, nil From 16b9c689f4012549201eb806ec7d83784994f244 Mon Sep 17 00:00:00 2001 From: joce Date: Mon, 21 Feb 2022 22:38:14 -0500 Subject: [PATCH 06/11] Save the `.moprc` file in a cleaner way. --- profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profile.go b/profile.go index 29b8079..c876492 100644 --- a/profile.go +++ b/profile.go @@ -98,7 +98,7 @@ func NewProfile(filename string) *Profile { // Save serializes settings using JSON and saves them in ~/.moprc file. func (profile *Profile) Save() error { - data, err := json.Marshal(profile) + data, err := json.MarshalIndent(profile, "", " ") if err != nil { return err } From dbd989c8947a8d7154b1dd12dc8c36ebefb5eb55 Mon Sep 17 00:00:00 2001 From: joce Date: Mon, 21 Feb 2022 22:39:20 -0500 Subject: [PATCH 07/11] Add semantic markup --- cmd/mop/main.go | 4 ++-- layout.go | 12 ++++++------ markup.go | 8 +++++++- screen.go | 4 ++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cmd/mop/main.go b/cmd/mop/main.go index 00eb8bf..5b7cc4a 100644 --- a/cmd/mop/main.go +++ b/cmd/mop/main.go @@ -138,10 +138,10 @@ func main() { profileName := flag.String("profile", path.Join(usr.HomeDir, defaultProfile), "path to profile") flag.Parse() - screen := mop.NewScreen() + profile := mop.NewProfile(*profileName) + screen := mop.NewScreen(profile) defer screen.Close() - profile := mop.NewProfile(*profileName) mainLoop(screen, profile) profile.Save() } diff --git a/layout.go b/layout.go index 7f57ae4..5478981 100644 --- a/layout.go +++ b/layout.go @@ -209,9 +209,9 @@ func (layout *Layout) pad(str string, width int) string { //----------------------------------------------------------------------------- func buildMarketTemplate() *template.Template { - markup := `Dow {{.Dow.change}} ({{.Dow.percent}}) at {{.Dow.latest}} S&P 500 {{.Sp500.change}} ({{.Sp500.percent}}) at {{.Sp500.latest}} NASDAQ {{.Nasdaq.change}} ({{.Nasdaq.percent}}) at {{.Nasdaq.latest}} -Tokyo {{.Tokyo.change}} ({{.Tokyo.percent}}) at {{.Tokyo.latest}} HK {{.HongKong.change}} ({{.HongKong.percent}}) at {{.HongKong.latest}} London {{.London.change}} ({{.London.percent}}) at {{.London.latest}} Frankfurt {{.Frankfurt.change}} ({{.Frankfurt.percent}}) at {{.Frankfurt.latest}} {{if .IsClosed}}U.S. markets closed{{end}} -10-Year Yield {{.Yield.latest}} ({{.Yield.change}}) Euro ${{.Euro.latest}} ({{.Euro.change}}) Yen ¥{{.Yen.latest}} ({{.Yen.change}}) Oil ${{.Oil.latest}} ({{.Oil.change}}) Gold ${{.Gold.latest}} ({{.Gold.change}})` + markup := `Dow {{.Dow.change}} ({{.Dow.percent}}) at {{.Dow.latest}} S&P 500 {{.Sp500.change}} ({{.Sp500.percent}}) at {{.Sp500.latest}} NASDAQ {{.Nasdaq.change}} ({{.Nasdaq.percent}}) at {{.Nasdaq.latest}} +Tokyo {{.Tokyo.change}} ({{.Tokyo.percent}}) at {{.Tokyo.latest}} HK {{.HongKong.change}} ({{.HongKong.percent}}) at {{.HongKong.latest}} London {{.London.change}} ({{.London.percent}}) at {{.London.latest}} Frankfurt {{.Frankfurt.change}} ({{.Frankfurt.percent}}) at {{.Frankfurt.latest}} {{if .IsClosed}}U.S. markets closed{{end}} +10-Year Yield {{.Yield.latest}} ({{.Yield.change}}) Euro ${{.Euro.latest}} ({{.Euro.change}}) Yen ¥{{.Yen.latest}} ({{.Yen.change}}) Oil ${{.Oil.latest}} ({{.Oil.change}}) Gold ${{.Gold.latest}} ({{.Gold.change}})` return template.Must(template.New(`market`).Parse(markup)) } @@ -223,7 +223,7 @@ func buildQuotesTemplate() *template.Template { {{.Header}} -{{range.Stocks}}{{if eq .Direction 1}}{{else if eq .Direction -1}}{{end}}{{.Ticker}}{{.LastTrade}}{{.Change}}{{.ChangePct}}{{.Open}}{{.Low}}{{.High}}{{.Low52}}{{.High52}}{{.Volume}}{{.AvgVolume}}{{.PeRatio}}{{.Dividend}}{{.Yield}}{{.MarketCap}}{{.PreOpen}}{{.AfterHours}} +{{range.Stocks}}{{if eq .Direction 1}}{{else if eq .Direction -1}}{{end}}{{.Ticker}}{{.LastTrade}}{{.Change}}{{.ChangePct}}{{.Open}}{{.Low}}{{.High}}{{.Low52}}{{.High52}}{{.Volume}}{{.AvgVolume}}{{.PeRatio}}{{.Dividend}}{{.Yield}}{{.MarketCap}}{{.PreOpen}}{{.AfterHours}} {{end}}` return template.Must(template.New(`quotes`).Parse(markup)) @@ -239,9 +239,9 @@ func highlight(collections ...map[string]string) { adv, err := strconv.ParseFloat(change, 64) if err == nil { if adv < 0.0 { - collection[`change`] = `` + collection[`change`] + `` + collection[`change`] = `` + collection[`change`] + `` } else if adv > 0.0 { - collection[`change`] = `` + collection[`change`] + `` + collection[`change`] = `` + collection[`change`] + `` } } } diff --git a/markup.go b/markup.go index 4bf5e64..461cdb3 100644 --- a/markup.go +++ b/markup.go @@ -33,7 +33,7 @@ type Markup struct { // Creates markup to define tag to Termbox translation rules and store default // colors and column alignments. -func NewMarkup() *Markup { +func NewMarkup(profile *Profile) *Markup { markup := &Markup{} markup.Foreground = termbox.ColorDefault markup.Background = termbox.ColorDefault @@ -53,6 +53,12 @@ func NewMarkup() *Markup { markup.tags[`b`] = termbox.AttrBold // Attribute = 1 << (iota + 4) markup.tags[`u`] = termbox.AttrUnderline markup.tags[`r`] = termbox.AttrReverse + + // Semantic markups + markup.tags[`gain`] = markup.tags[profile.TickerColors.Gain] + markup.tags[`loss`] = markup.tags[profile.TickerColors.Loss] + markup.tags[`tag`] = markup.tags[profile.TickerColors.Tag] + markup.regex = markup.supportedTags() // Once we have the hash we could build the regex. return markup diff --git a/screen.go b/screen.go index 2c92614..91a2932 100644 --- a/screen.go +++ b/screen.go @@ -26,13 +26,13 @@ type Screen struct { // Initializes Termbox, creates screen along with layout and markup, and // calculates current screen dimensions. Once initialized the screen is // ready for display. -func NewScreen() *Screen { +func NewScreen(profile *Profile) *Screen { if err := termbox.Init(); err != nil { panic(err) } screen := &Screen{} screen.layout = NewLayout() - screen.markup = NewMarkup() + screen.markup = NewMarkup(profile) return screen.Resize() } From d6b0039c90ef6d7cba6be12a41c48ca264660378 Mon Sep 17 00:00:00 2001 From: joce Date: Mon, 21 Feb 2022 23:14:53 -0500 Subject: [PATCH 08/11] `Profile.TickerColors` => `Profile.Colors` --- markup.go | 6 +++--- profile.go | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/markup.go b/markup.go index 461cdb3..465e40c 100644 --- a/markup.go +++ b/markup.go @@ -55,9 +55,9 @@ func NewMarkup(profile *Profile) *Markup { markup.tags[`r`] = termbox.AttrReverse // Semantic markups - markup.tags[`gain`] = markup.tags[profile.TickerColors.Gain] - markup.tags[`loss`] = markup.tags[profile.TickerColors.Loss] - markup.tags[`tag`] = markup.tags[profile.TickerColors.Tag] + markup.tags[`gain`] = markup.tags[profile.Colors.Gain] + markup.tags[`loss`] = markup.tags[profile.Colors.Loss] + markup.tags[`tag`] = markup.tags[profile.Colors.Tag] markup.regex = markup.supportedTags() // Once we have the hash we could build the regex. diff --git a/profile.go b/profile.go index c876492..7dbd62c 100644 --- a/profile.go +++ b/profile.go @@ -28,7 +28,7 @@ type Profile struct { Ascending bool // True when sort order is ascending. Grouped bool // True when stocks are grouped by advancing/declining. Filter string // Filter in human form - TickerColors struct { // Ticker colors + Colors struct { // User defined colors Gain string Loss string Tag string @@ -67,26 +67,26 @@ func NewProfile(filename string) *Profile { profile.SortColumn = 0 // Stock quotes are sorted by ticker name. profile.Ascending = true // A to Z. profile.Filter = "" - profile.TickerColors.Gain = defaultGainColor - profile.TickerColors.Loss = defaultLossColor - profile.TickerColors.Tag = defaultTagColor + profile.Colors.Gain = defaultGainColor + profile.Colors.Loss = defaultLossColor + profile.Colors.Tag = defaultTagColor profile.Save() } else { json.Unmarshal(data, profile) - profile.TickerColors.Gain = strings.ToLower(profile.TickerColors.Gain) - if !IsSupportedColor(profile.TickerColors.Gain) { - profile.TickerColors.Gain = defaultGainColor + profile.Colors.Gain = strings.ToLower(profile.Colors.Gain) + if !IsSupportedColor(profile.Colors.Gain) { + profile.Colors.Gain = defaultGainColor } - profile.TickerColors.Loss = strings.ToLower(profile.TickerColors.Loss) - if !IsSupportedColor(profile.TickerColors.Loss) { - profile.TickerColors.Loss = defaultLossColor + profile.Colors.Loss = strings.ToLower(profile.Colors.Loss) + if !IsSupportedColor(profile.Colors.Loss) { + profile.Colors.Loss = defaultLossColor } - profile.TickerColors.Tag = strings.ToLower(profile.TickerColors.Tag) - if !IsSupportedColor(profile.TickerColors.Tag) { - profile.TickerColors.Tag = defaultTagColor + profile.Colors.Tag = strings.ToLower(profile.Colors.Tag) + if !IsSupportedColor(profile.Colors.Tag) { + profile.Colors.Tag = defaultTagColor } profile.SetFilter(profile.Filter) From 9560670e98ed37640919518da1c65888c5dfd08a Mon Sep 17 00:00:00 2001 From: joce Date: Tue, 22 Feb 2022 16:48:09 -0500 Subject: [PATCH 09/11] Add light colors as well for markup --- markup.go | 9 +++++++++ profile.go | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/markup.go b/markup.go index 465e40c..c99dbb4 100644 --- a/markup.go +++ b/markup.go @@ -49,6 +49,15 @@ func NewMarkup(profile *Profile) *Markup { markup.tags[`magenta`] = termbox.ColorMagenta markup.tags[`cyan`] = termbox.ColorCyan markup.tags[`white`] = termbox.ColorWhite + markup.tags[`darkgray`] = termbox.ColorDarkGray + markup.tags[`lightred`] = termbox.ColorLightRed + markup.tags[`lightgreen`] = termbox.ColorLightGreen + markup.tags[`lightyellow`] = termbox.ColorLightYellow + markup.tags[`lightblue`] = termbox.ColorLightBlue + markup.tags[`lightmagenta`] = termbox.ColorLightMagenta + markup.tags[`lightcyan`] = termbox.ColorLightCyan + markup.tags[`lightgray`] = termbox.ColorLightGray + markup.tags[`right`] = termbox.ColorDefault // Termbox can combine attributes and a single color using bitwise OR. markup.tags[`b`] = termbox.AttrBold // Attribute = 1 << (iota + 4) markup.tags[`u`] = termbox.AttrUnderline diff --git a/profile.go b/profile.go index 7dbd62c..fc5fe04 100644 --- a/profile.go +++ b/profile.go @@ -48,7 +48,15 @@ func IsSupportedColor(colorName string) bool { "blue", "magenta", "cyan", - "white": + "white", + "darkgray", + "lightred", + "lightgreen", + "lightyellow", + "lightblue", + "lightmagenta", + "lightcyan", + "lightgray": return true } return false From b45fbfe90815b2b4eb62ed2e31183529fe932491 Mon Sep 17 00:00:00 2001 From: joce Date: Wed, 23 Feb 2022 17:28:42 -0500 Subject: [PATCH 10/11] Factorize code to init sementic colors --- profile.go | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/profile.go b/profile.go index fc5fe04..4be37d0 100644 --- a/profile.go +++ b/profile.go @@ -82,20 +82,9 @@ func NewProfile(filename string) *Profile { } else { json.Unmarshal(data, profile) - profile.Colors.Gain = strings.ToLower(profile.Colors.Gain) - if !IsSupportedColor(profile.Colors.Gain) { - profile.Colors.Gain = defaultGainColor - } - - profile.Colors.Loss = strings.ToLower(profile.Colors.Loss) - if !IsSupportedColor(profile.Colors.Loss) { - profile.Colors.Loss = defaultLossColor - } - - profile.Colors.Tag = strings.ToLower(profile.Colors.Tag) - if !IsSupportedColor(profile.Colors.Tag) { - profile.Colors.Tag = defaultTagColor - } + InitColor(profile.Colors.Gain, defaultGainColor) + InitColor(profile.Colors.Loss, defaultLossColor) + InitColor(profile.Colors.Tag, defaultTagColor) profile.SetFilter(profile.Filter) } @@ -104,6 +93,13 @@ func NewProfile(filename string) *Profile { return profile } +func InitColor(color string, defaultValue string) { + color = strings.ToLower(color) + if !IsSupportedColor(color) { + color = defaultValue; + } +} + // Save serializes settings using JSON and saves them in ~/.moprc file. func (profile *Profile) Save() error { data, err := json.MarshalIndent(profile, "", " ") From 3df70dc52cd25b4e0ec78c3aeb8191d6e5f5393a Mon Sep 17 00:00:00 2001 From: joce Date: Wed, 9 Mar 2022 21:32:30 -0500 Subject: [PATCH 11/11] Add sementic colors for time, header and "default" --- cmd/mop/main.go | 2 +- layout.go | 12 +++---- markup.go | 20 +++++++----- profile.go | 84 ++++++++++++++++++++++++++++--------------------- screen.go | 25 ++++++++------- 5 files changed, 81 insertions(+), 62 deletions(-) diff --git a/cmd/mop/main.go b/cmd/mop/main.go index 5b7cc4a..2418a52 100644 --- a/cmd/mop/main.go +++ b/cmd/mop/main.go @@ -143,5 +143,5 @@ func main() { defer screen.Close() mainLoop(screen, profile) - profile.Save() + profile.Save() } diff --git a/layout.go b/layout.go index 5478981..7d495ae 100644 --- a/layout.go +++ b/layout.go @@ -168,7 +168,7 @@ func (layout *Layout) prettify(quotes *Quotes) []Stock { profile := quotes.profile - if profile.Filter != ""{ // Fix for blank display if invalid filter expression was cleared. + if profile.Filter != "" { // Fix for blank display if invalid filter expression was cleared. if profile.filterExpression != nil { if layout.filter == nil { // Initialize filter on first invocation. layout.filter = NewFilter(profile) @@ -218,11 +218,11 @@ func buildMarketTemplate() *template.Template { //----------------------------------------------------------------------------- func buildQuotesTemplate() *template.Template { - markup := `{{.Now}} + markup := ` -{{.Header}} +
{{.Header}} {{range.Stocks}}{{if eq .Direction 1}}{{else if eq .Direction -1}}{{end}}{{.Ticker}}{{.LastTrade}}{{.Change}}{{.ChangePct}}{{.Open}}{{.Low}}{{.High}}{{.Low52}}{{.High52}}{{.Volume}}{{.AvgVolume}}{{.PeRatio}}{{.Dividend}}{{.Yield}}{{.MarketCap}}{{.PreOpen}}{{.AfterHours}} {{end}}` @@ -234,7 +234,7 @@ func highlight(collections ...map[string]string) { for _, collection := range collections { change := collection[`change`] if change[len(change)-1:] == `%` { - change = change[0:len(change)-1] + change = change[0 : len(change)-1] } adv, err := strconv.ParseFloat(change, 64) if err == nil { @@ -272,9 +272,9 @@ func group(stocks []Stock) []Stock { func arrowFor(column int, profile *Profile) string { if column == profile.SortColumn { if profile.Ascending { - return string('\U00002191') + return string('▲') } - return string('\U00002193') + return string('▼') } return `` } diff --git a/markup.go b/markup.go index c99dbb4..331926f 100644 --- a/markup.go +++ b/markup.go @@ -5,9 +5,10 @@ package mop import ( - `github.com/nsf/termbox-go` - `regexp` - `strings` + "regexp" + "strings" + + "github.com/nsf/termbox-go" ) // Markup implements some minimalistic text formatting conventions that @@ -35,9 +36,6 @@ type Markup struct { // colors and column alignments. func NewMarkup(profile *Profile) *Markup { markup := &Markup{} - markup.Foreground = termbox.ColorDefault - markup.Background = termbox.ColorDefault - markup.RightAligned = false markup.tags = make(map[string]termbox.Attribute) markup.tags[`/`] = termbox.ColorDefault @@ -67,6 +65,14 @@ func NewMarkup(profile *Profile) *Markup { markup.tags[`gain`] = markup.tags[profile.Colors.Gain] markup.tags[`loss`] = markup.tags[profile.Colors.Loss] markup.tags[`tag`] = markup.tags[profile.Colors.Tag] + markup.tags[`header`] = markup.tags[profile.Colors.Header] + markup.tags[`time`] = markup.tags[profile.Colors.Time] + markup.tags[`default`] = markup.tags[profile.Colors.Default] + + markup.Foreground = markup.tags[profile.Colors.Default] + + markup.Background = termbox.ColorDefault + markup.RightAligned = false markup.regex = markup.supportedTags() // Once we have the hash we could build the regex. @@ -138,7 +144,7 @@ func (markup *Markup) process(tag string, open bool) bool { if attribute >= termbox.AttrBold { markup.Foreground &= ^attribute // Clear the Termbox attribute. } else { - markup.Foreground = termbox.ColorDefault + markup.Foreground = markup.tags[`default`] } } } diff --git a/profile.go b/profile.go index 4be37d0..d6a3779 100644 --- a/profile.go +++ b/profile.go @@ -16,22 +16,28 @@ import ( const defaultGainColor = "green" const defaultLossColor = "red" const defaultTagColor = "yellow" +const defaultHeaderColor = "lightgray" +const defaultTimeColor = "lightgray" +const defaultColor = "lightgray" // Profile manages Mop program settings as defined by user (ex. list of // stock tickers). The settings are serialized using JSON and saved in // the ~/.moprc file. type Profile struct { - Tickers []string // List of stock tickers to display. - MarketRefresh int // Time interval to refresh market data. - QuotesRefresh int // Time interval to refresh stock quotes. - SortColumn int // Column number by which we sort stock quotes. - Ascending bool // True when sort order is ascending. - Grouped bool // True when stocks are grouped by advancing/declining. - Filter string // Filter in human form - Colors struct { // User defined colors - Gain string - Loss string - Tag string + Tickers []string // List of stock tickers to display. + MarketRefresh int // Time interval to refresh market data. + QuotesRefresh int // Time interval to refresh stock quotes. + SortColumn int // Column number by which we sort stock quotes. + Ascending bool // True when sort order is ascending. + Grouped bool // True when stocks are grouped by advancing/declining. + Filter string // Filter in human form + Colors struct { // User defined colors + Gain string + Loss string + Tag string + Header string + Time string + Default string } filterExpression *govaluate.EvaluableExpression // The filter as a govaluate expression selectedColumn int // Stores selected column number when the column editor is active. @@ -40,24 +46,24 @@ type Profile struct { func IsSupportedColor(colorName string) bool { switch colorName { - case - "black", - "red", - "green", - "yellow", - "blue", - "magenta", - "cyan", - "white", - "darkgray", - "lightred", - "lightgreen", - "lightyellow", - "lightblue", - "lightmagenta", - "lightcyan", - "lightgray": - return true + case + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "darkgray", + "lightred", + "lightgreen", + "lightyellow", + "lightblue", + "lightmagenta", + "lightcyan", + "lightgray": + return true } return false } @@ -78,13 +84,19 @@ func NewProfile(filename string) *Profile { profile.Colors.Gain = defaultGainColor profile.Colors.Loss = defaultLossColor profile.Colors.Tag = defaultTagColor + profile.Colors.Header = defaultHeaderColor + profile.Colors.Time = defaultTimeColor + profile.Colors.Default = defaultColor profile.Save() } else { json.Unmarshal(data, profile) - InitColor(profile.Colors.Gain, defaultGainColor) - InitColor(profile.Colors.Loss, defaultLossColor) - InitColor(profile.Colors.Tag, defaultTagColor) + InitColor(&profile.Colors.Gain, defaultGainColor) + InitColor(&profile.Colors.Loss, defaultLossColor) + InitColor(&profile.Colors.Tag, defaultTagColor) + InitColor(&profile.Colors.Header, defaultHeaderColor) + InitColor(&profile.Colors.Time, defaultTimeColor) + InitColor(&profile.Colors.Default, defaultColor) profile.SetFilter(profile.Filter) } @@ -93,10 +105,10 @@ func NewProfile(filename string) *Profile { return profile } -func InitColor(color string, defaultValue string) { - color = strings.ToLower(color) - if !IsSupportedColor(color) { - color = defaultValue; +func InitColor(color *string, defaultValue string) { + *color = strings.ToLower(*color) + if !IsSupportedColor(*color) { + *color = defaultValue } } diff --git a/screen.go b/screen.go index 91a2932..9a9b45d 100644 --- a/screen.go +++ b/screen.go @@ -5,11 +5,12 @@ package mop import ( - `github.com/nsf/termbox-go` - `strings` - `time` - `strconv` - `fmt` + "fmt" + "strconv" + "strings" + "time" + + "github.com/nsf/termbox-go" ) // Screen is thin wrapper around Termbox library to provide basic display @@ -90,7 +91,7 @@ func (screen *Screen) ClearLine(x int, y int) *Screen { func (screen *Screen) Draw(objects ...interface{}) *Screen { zonename, _ := time.Now().In(time.Local).Zone() if screen.pausedAt != nil { - defer screen.DrawLine(0, 0, ``+screen.pausedAt.Format(`3:04:05pm ` + zonename)+``) + defer screen.DrawLine(0, 0, ``+screen.pausedAt.Format(`3:04:05pm `+zonename)+``) } for _, ptr := range objects { switch ptr.(type) { @@ -102,7 +103,7 @@ func (screen *Screen) Draw(objects ...interface{}) *Screen { screen.draw(screen.layout.Quotes(object.Fetch())) case time.Time: timestamp := ptr.(time.Time).Format(`3:04:05pm ` + zonename) - screen.DrawLine(0, 0, ``+timestamp+``) + screen.DrawLine(0, 0, ``) default: screen.draw(ptr.(string)) } @@ -146,7 +147,7 @@ func (screen *Screen) draw(str string) { drewHeading := false tempFormat := "%" + strconv.Itoa(screen.width) + "s" - blankLine := fmt.Sprintf(tempFormat,"") + blankLine := fmt.Sprintf(tempFormat, "") allLines = strings.Split(str, "\n") // Write the lines being updated. @@ -154,9 +155,9 @@ func (screen *Screen) draw(str string) { screen.DrawLine(0, row, allLines[row]) // Did we draw the underlined heading row? This is a crude // check, but--see comments below... - if strings.Contains(allLines[row],"Ticker") && - strings.Contains(allLines[row],"Last") && - strings.Contains(allLines[row],"Change") { + if strings.Contains(allLines[row], "Ticker") && + strings.Contains(allLines[row], "Last") && + strings.Contains(allLines[row], "Change") { drewHeading = true } } @@ -173,7 +174,7 @@ func (screen *Screen) draw(str string) { // cycle. In that case, padding with blank lines would overwrite the // stocks list.) if drewHeading { - for i := len(allLines)-1; i < screen.height; i++ { + for i := len(allLines) - 1; i < screen.height; i++ { screen.DrawLine(0, i, blankLine) } }