master
Michael Dvorkin 10 years ago
parent 7932776cf5
commit bc666ec165
  1. 2
      LICENSE
  2. 2
      README.md
  3. 4
      cmd/mop.go
  4. 90
      cnn_market.go
  5. 21
      column_editor.go
  6. 78
      layout.go
  7. 44
      line_editor.go
  8. 54
      markup.go
  9. 34
      profile.go
  10. 38
      screen.go
  11. 253
      sorter.go
  12. 54
      yahoo_quotes.go

@ -1,4 +1,4 @@
Copyright (c) 2013 Michael Dvorkin. All Rights Reserved.
Copyright (c) 2013-2015 Michael Dvorkin. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

@ -44,7 +44,7 @@ comments, suggestions, and contributions are welcome.
### License ###
Copyright (c) 2013 Michael Dvorkin. All Rights Reserved.
Copyright (c) 2013-2015 Michael Dvorkin. All Rights Reserved.
"mike" + "@dvorkin" + ".net" || "twitter.com/mid"
Permission is hereby granted, free of charge, to any person obtaining

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -10,7 +10,7 @@ import (
`time`
)
const help = `Mop v0.2.0 -- Copyright (c) 2013-15 Michael Dvorkin. All Rights Reserved.
const help = `Mop v0.2.0 -- Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
NO WARRANTIES OF ANY KIND WHATSOEVER. SEE THE LICENSE FILE FOR DETAILS.
<u>Command</u> <u>Description </u>

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -18,61 +18,61 @@ const marketURL = `http://money.cnn.com/data/markets/`
// Market stores current market information displayed in the top three lines of
// the screen. The market data is fetched and parsed from the HTML page above.
type Market struct {
IsClosed bool // True when U.S. markets are closed.
Dow map[string]string // Hash of Dow Jones indicators.
Nasdaq map[string]string // Hash of NASDAQ indicators.
Sp500 map[string]string // Hash of S&P 500 indicators.
Tokyo map[string]string
HongKong map[string]string
London map[string]string
Frankfurt map[string]string
Yield map[string]string
Oil map[string]string
Yen map[string]string
Euro map[string]string
Gold map[string]string
regex *regexp.Regexp // Regex to parse market data from HTML.
errors string // Error(s), if any.
IsClosed bool // True when U.S. markets are closed.
Dow map[string]string // Hash of Dow Jones indicators.
Nasdaq map[string]string // Hash of NASDAQ indicators.
Sp500 map[string]string // Hash of S&P 500 indicators.
Tokyo map[string]string
HongKong map[string]string
London map[string]string
Frankfurt map[string]string
Yield map[string]string
Oil map[string]string
Yen map[string]string
Euro map[string]string
Gold map[string]string
regex *regexp.Regexp // Regex to parse market data from HTML.
errors string // Error(s), if any.
}
// Returns new initialized Market struct.
func NewMarket() *Market {
market := &Market{};
market.IsClosed = false
market.Dow = make(map[string]string)
market.Nasdaq = make(map[string]string)
market.Sp500 = make(map[string]string)
market.Tokyo = make(map[string]string)
market.HongKong = make(map[string]string)
market.London = make(map[string]string)
market := &Market{}
market.IsClosed = false
market.Dow = make(map[string]string)
market.Nasdaq = make(map[string]string)
market.Sp500 = make(map[string]string)
market.Tokyo = make(map[string]string)
market.HongKong = make(map[string]string)
market.London = make(map[string]string)
market.Frankfurt = make(map[string]string)
market.Yield = make(map[string]string)
market.Oil = make(map[string]string)
market.Yen = make(map[string]string)
market.Euro = make(map[string]string)
market.Gold = make(map[string]string)
market.Yield = make(map[string]string)
market.Oil = make(map[string]string)
market.Yen = make(map[string]string)
market.Euro = make(map[string]string)
market.Gold = make(map[string]string)
market.errors = ``
market.errors = ``
const any = `\s*(?:.+?)`
const price = `>([\d\.,]+)</span>`
const percent = `>([\+\-]?[\d\.,]+%?)<`
rules := []string{
`>Dow<`, any, percent, any, price, any, percent, any,
`>Nasdaq<`, any, percent, any, price, any, percent, any,
`">S&P<`, any, percent, any, price, any, percent, any,
`>Dow<`, any, percent, any, price, any, percent, any,
`>Nasdaq<`, any, percent, any, price, any, percent, any,
`">S&P<`, any, percent, any, price, any, percent, any,
`>Nikkei 225<`, any, percent, any, price, any, percent, any,
`>Hang Seng<`, any, percent, any, price, any, percent, any,
`>FTSE 100<`, any, percent, any, price, any, percent, any,
`>DAX<`, any, percent, any, price, any, percent, any,
`>10-year yield<`, any, price, any, percent, any,
`>Oil<`, any, price, any, percent, any,
`>Yen<`, any, price, any, percent, any,
`>Euro<`, any, price, any, percent, any,
`>Gold<`, any, price, any, percent, any,
`>Hang Seng<`, any, percent, any, price, any, percent, any,
`>FTSE 100<`, any, percent, any, price, any, percent, any,
`>DAX<`, any, percent, any, price, any, percent, any,
`>10-year yield<`, any, price, any, percent, any,
`>Oil<`, any, price, any, percent, any,
`>Yen<`, any, price, any, percent, any,
`>Euro<`, any, price, any, percent, any,
`>Gold<`, any, price, any, percent, any,
}
market.regex = regexp.MustCompile(strings.Join(rules, ``))
@ -131,9 +131,9 @@ func (market *Market) trim(body []byte) []byte {
func (market *Market) extract(snippet []byte) *Market {
matches := market.regex.FindStringSubmatch(string(snippet))
if len(matches) < 31 {
panic(`Unable to parse ` + marketURL)
}
if len(matches) < 31 {
panic(`Unable to parse ` + marketURL)
}
market.Dow[`change`] = matches[1]
market.Dow[`latest`] = matches[2]

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -10,19 +10,19 @@ import `github.com/michaeldv/termbox-go`
// current column name in the header, then waits for arrow keys (choose
// another column), Enter (reverse sort order), or Esc (exit).
type ColumnEditor struct {
screen *Screen // Pointer to Screen so we could use screen.Draw().
quotes *Quotes // Pointer to Quotes to redraw them when the sort order changes.
layout *Layout // Pointer to Layout to redraw stock quotes header.
profile *Profile // Pointer to Profile where we save newly selected sort order.
screen *Screen // Pointer to Screen so we could use screen.Draw().
quotes *Quotes // Pointer to Quotes to redraw them when the sort order changes.
layout *Layout // Pointer to Layout to redraw stock quotes header.
profile *Profile // Pointer to Profile where we save newly selected sort order.
}
// Returns new initialized ColumnEditor struct. As part of initialization it
// highlights current column name (as stored in Profile).
func NewColumnEditor(screen *Screen, quotes *Quotes) *ColumnEditor {
editor := &ColumnEditor{
screen: screen,
quotes: quotes,
layout: screen.layout,
screen: screen,
quotes: quotes,
layout: screen.layout,
profile: quotes.profile,
}
@ -43,7 +43,7 @@ func (editor *ColumnEditor) Handle(event termbox.Event) bool {
case termbox.KeyEnter:
editor.execute()
case termbox.KeyArrowLeft:
case termbox.KeyArrowLeft:
editor.selectLeftColumn()
case termbox.KeyArrowRight:
@ -72,7 +72,7 @@ func (editor *ColumnEditor) selectLeftColumn() *ColumnEditor {
//-----------------------------------------------------------------------------
func (editor *ColumnEditor) selectRightColumn() *ColumnEditor {
editor.profile.selectedColumn++
if editor.profile.selectedColumn > editor.layout.TotalColumns() - 1 {
if editor.profile.selectedColumn > editor.layout.TotalColumns()-1 {
editor.profile.selectedColumn = 0
}
return editor
@ -98,4 +98,3 @@ func (editor *ColumnEditor) redrawHeader() {
editor.screen.DrawLine(0, 4, editor.layout.Header(editor.profile))
termbox.Flush()
}

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -17,41 +17,41 @@ import (
// Column describes formatting rules for individual column within the list
// of stock quotes.
type Column struct {
width int // Column width.
name string // The name of the field in the Stock struct.
title string // Column title to display in the header.
formatter func(string)string // Optional function to format the contents of the column.
width int // Column width.
name string // The name of the field in the Stock struct.
title string // Column title to display in the header.
formatter func(string) string // Optional function to format the contents of the column.
}
// Layout is used to format and display all the collected data, i.e. market
// updates and the list of stock quotes.
type Layout struct {
columns []Column // List of stock quotes columns.
sorter *Sorter // Pointer to sorting receiver.
regex *regexp.Regexp // Pointer to regular expression to align decimal points.
marketTemplate *template.Template // Pointer to template to format market data.
quotesTemplate *template.Template // Pointer to template to format the list of stock quotes.
columns []Column // List of stock quotes columns.
sorter *Sorter // Pointer to sorting receiver.
regex *regexp.Regexp // Pointer to regular expression to align decimal points.
marketTemplate *template.Template // Pointer to template to format market data.
quotesTemplate *template.Template // Pointer to template to format the list of stock quotes.
}
// Creates the layout and assigns the default values that stay unchanged.
func NewLayout() *Layout {
layout := &Layout{}
layout.columns = []Column{
{ -7, `Ticker`, `Ticker`, nil },
{ 10, `LastTrade`, `Last`, currency },
{ 10, `Change`, `Change`, currency },
{ 10, `ChangePct`, `Change%`, last },
{ 10, `Open`, `Open`, currency },
{ 10, `Low`, `Low`, currency },
{ 10, `High`, `High`, currency },
{ 10, `Low52`, `52w Low`, currency },
{ 10, `High52`, `52w High`, currency },
{ 11, `Volume`, `Volume`, nil },
{ 11, `AvgVolume`, `AvgVolume`, nil },
{ 9, `PeRatio`, `P/E`, blank },
{ 9, `Dividend`, `Dividend`, zero },
{ 9, `Yield`, `Yield`, percent },
{ 11, `MarketCap`, `MktCap`, currency },
{-7, `Ticker`, `Ticker`, nil},
{10, `LastTrade`, `Last`, currency},
{10, `Change`, `Change`, currency},
{10, `ChangePct`, `Change%`, last},
{10, `Open`, `Open`, currency},
{10, `Low`, `Low`, currency},
{10, `High`, `High`, currency},
{10, `Low52`, `52w Low`, currency},
{10, `High52`, `52w High`, currency},
{11, `Volume`, `Volume`, nil},
{11, `AvgVolume`, `AvgVolume`, nil},
{9, `PeRatio`, `P/E`, blank},
{9, `Dividend`, `Dividend`, zero},
{9, `Yield`, `Yield`, percent},
{11, `MarketCap`, `MktCap`, currency},
}
layout.regex = regexp.MustCompile(`(\.\d+)[BMK]?$`)
layout.marketTemplate = buildMarketTemplate()
@ -63,8 +63,8 @@ func NewLayout() *Layout {
// Market merges given market data structure with the market template and
// returns formatted string that includes highlighting markup.
func (layout *Layout) Market(market *Market) string {
if ok, err := market.Ok(); !ok { // If there was an error fetching market data...
return err // then simply return the error string.
if ok, err := market.Ok(); !ok { // If there was an error fetching market data...
return err // then simply return the error string.
}
highlight(market.Dow, market.Sp500, market.Nasdaq,
@ -80,14 +80,14 @@ func (layout *Layout) Market(market *Market) string {
// and the list of given stock quotes. It returns formatted string with
// all the necessary markup.
func (layout *Layout) Quotes(quotes *Quotes) string {
if ok, err := quotes.Ok(); !ok { // If there was an error fetching stock quotes...
return err // then simply return the error string.
if ok, err := quotes.Ok(); !ok { // If there was an error fetching stock quotes...
return err // then simply return the error string.
}
vars := struct {
Now string // Current timestamp.
Header string // Formatted header line.
Stocks []Stock // List of formatted stock quotes.
Now string // Current timestamp.
Header string // Formatted header line.
Stocks []Stock // List of formatted stock quotes.
}{
time.Now().Format(`3:04:05pm PST`),
layout.Header(quotes.profile),
@ -107,12 +107,12 @@ func (layout *Layout) Quotes(quotes *Quotes) string {
func (layout *Layout) Header(profile *Profile) string {
str, selectedColumn := ``, profile.selectedColumn
for i,col := range layout.columns {
for i, col := range layout.columns {
arrow := arrowFor(i, profile)
if i != selectedColumn {
str += fmt.Sprintf(`%*s`, col.width, arrow + col.title)
str += fmt.Sprintf(`%*s`, col.width, arrow+col.title)
} else {
str += fmt.Sprintf(`<r>%*s</r>`, col.width, arrow + col.title)
str += fmt.Sprintf(`<r>%*s</r>`, col.width, arrow+col.title)
}
}
@ -139,7 +139,7 @@ func (layout *Layout) prettify(quotes *Quotes) []Stock {
// - If the column has the formatter method then call it.
// - Set the column value padding it to the given width.
//
for _,column := range layout.columns {
for _, column := range layout.columns {
// ex. value = stock.Change
value := reflect.ValueOf(&stock).Elem().FieldByName(column.name).String()
if column.formatter != nil {
@ -173,7 +173,7 @@ func (layout *Layout) pad(str string, width int) string {
if len(match) > 0 {
switch len(match[1]) {
case 2:
str = strings.Replace(str, match[1], match[1] + `0`, 1)
str = strings.Replace(str, match[1], match[1]+`0`, 1)
case 4, 5:
str = strings.Replace(str, match[1], match[1][0:3], 1)
}
@ -218,13 +218,13 @@ func group(stocks []Stock) []Stock {
grouped := make([]Stock, len(stocks))
current := 0
for _,stock := range stocks {
for _, stock := range stocks {
if stock.Advancing {
grouped[current] = stock
current++
}
}
for _,stock := range stocks {
for _, stock := range stocks {
if !stock.Advancing {
grouped[current] = stock
current++

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -15,13 +15,13 @@ import (
// data and keep track of cursor movements (left, right, beginning of the
// line, end of the line, and backspace).
type LineEditor struct {
command rune // Keyboard command such as '+' or '-'.
cursor int // Current cursor position within the input line.
prompt string // Prompt string for the command.
input string // User typed input string.
screen *Screen // Pointer to Screen.
quotes *Quotes // Pointer to Quotes.
regex *regexp.Regexp // Regex to split comma-delimited input string.
command rune // Keyboard command such as '+' or '-'.
cursor int // Current cursor position within the input line.
prompt string // Prompt string for the command.
input string // User typed input string.
screen *Screen // Pointer to Screen.
quotes *Quotes // Pointer to Quotes.
regex *regexp.Regexp // Regex to split comma-delimited input string.
}
// Returns new initialized LineEditor struct.
@ -29,7 +29,7 @@ func NewLineEditor(screen *Screen, quotes *Quotes) *LineEditor {
return &LineEditor{
screen: screen,
quotes: quotes,
regex: regexp.MustCompile(`[,\s]+`),
regex: regexp.MustCompile(`[,\s]+`),
}
}
@ -42,7 +42,7 @@ func (editor *LineEditor) Prompt(command rune) *LineEditor {
editor.prompt = prompt
editor.command = command
editor.screen.DrawLine(0, 3, `<white>` + editor.prompt + `</>`)
editor.screen.DrawLine(0, 3, `<white>`+editor.prompt+`</>`)
termbox.SetCursor(len(editor.prompt), 3)
termbox.Flush()
}
@ -65,7 +65,7 @@ func (editor *LineEditor) Handle(ev termbox.Event) bool {
case termbox.KeyEnter:
return editor.execute().done()
case termbox.KeyBackspace, termbox.KeyBackspace2:
case termbox.KeyBackspace, termbox.KeyBackspace2:
editor.deletePreviousCharacter()
case termbox.KeyCtrlB, termbox.KeyArrowLeft:
@ -97,12 +97,12 @@ func (editor *LineEditor) deletePreviousCharacter() *LineEditor {
if editor.cursor > 0 {
if editor.cursor < len(editor.input) {
// Remove character in the middle of the input string.
editor.input = editor.input[0 : editor.cursor-1] + editor.input[editor.cursor : len(editor.input)]
editor.input = editor.input[0:editor.cursor-1] + editor.input[editor.cursor:len(editor.input)]
} else {
// Remove last input character.
editor.input = editor.input[ : len(editor.input)-1]
editor.input = editor.input[:len(editor.input)-1]
}
editor.screen.DrawLine(len(editor.prompt), 3, editor.input + ` `) // Erase last character.
editor.screen.DrawLine(len(editor.prompt), 3, editor.input+` `) // Erase last character.
editor.moveLeft()
}
@ -113,7 +113,7 @@ func (editor *LineEditor) deletePreviousCharacter() *LineEditor {
func (editor *LineEditor) insertCharacter(ch rune) *LineEditor {
if editor.cursor < len(editor.input) {
// Insert the character in the middle of the input string.
editor.input = editor.input[0 : editor.cursor] + string(ch) + editor.input[editor.cursor : len(editor.input)]
editor.input = editor.input[0:editor.cursor] + string(ch) + editor.input[editor.cursor:len(editor.input)]
} else {
// Append the character to the end of the input string.
editor.input += string(ch)
@ -128,7 +128,7 @@ func (editor *LineEditor) insertCharacter(ch rune) *LineEditor {
func (editor *LineEditor) moveLeft() *LineEditor {
if editor.cursor > 0 {
editor.cursor--
termbox.SetCursor(len(editor.prompt) + editor.cursor, 3)
termbox.SetCursor(len(editor.prompt)+editor.cursor, 3)
}
return editor
@ -138,7 +138,7 @@ func (editor *LineEditor) moveLeft() *LineEditor {
func (editor *LineEditor) moveRight() *LineEditor {
if editor.cursor < len(editor.input) {
editor.cursor++
termbox.SetCursor(len(editor.prompt) + editor.cursor, 3)
termbox.SetCursor(len(editor.prompt)+editor.cursor, 3)
}
return editor
@ -147,7 +147,7 @@ func (editor *LineEditor) moveRight() *LineEditor {
//-----------------------------------------------------------------------------
func (editor *LineEditor) jumpToBeginning() *LineEditor {
editor.cursor = 0
termbox.SetCursor(len(editor.prompt) + editor.cursor, 3)
termbox.SetCursor(len(editor.prompt)+editor.cursor, 3)
return editor
}
@ -155,7 +155,7 @@ func (editor *LineEditor) jumpToBeginning() *LineEditor {
//-----------------------------------------------------------------------------
func (editor *LineEditor) jumpToEnd() *LineEditor {
editor.cursor = len(editor.input)
termbox.SetCursor(len(editor.prompt) + editor.cursor, 3)
termbox.SetCursor(len(editor.prompt)+editor.cursor, 3)
return editor
}
@ -166,7 +166,7 @@ func (editor *LineEditor) execute() *LineEditor {
case '+':
tickers := editor.tokenize()
if len(tickers) > 0 {
if added,_ := editor.quotes.AddTickers(tickers); added > 0 {
if added, _ := editor.quotes.AddTickers(tickers); added > 0 {
editor.screen.Draw(editor.quotes)
}
}
@ -174,13 +174,13 @@ func (editor *LineEditor) execute() *LineEditor {
tickers := editor.tokenize()
if len(tickers) > 0 {
before := len(editor.quotes.profile.Tickers)
if removed,_ := editor.quotes.RemoveTickers(tickers); removed > 0 {
if removed, _ := editor.quotes.RemoveTickers(tickers); removed > 0 {
editor.screen.Draw(editor.quotes)
// Clear the lines at the bottom of the list, if any.
after := before - removed
for i := before; i > after; i-- {
editor.screen.ClearLine(0, i + 4)
editor.screen.ClearLine(0, i+4)
}
}
}

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -24,36 +24,36 @@ import (
// The <right>...</right> tag is used to right align the enclosed string
// (ex. when displaying current time in the upper right corner).
type Markup struct {
Foreground termbox.Attribute // Foreground color.
Background termbox.Attribute // Background color (so far always termbox.ColorDefault).
RightAligned bool // True when the string is right aligned.
tags map[string]termbox.Attribute // Tags to Termbox translation hash.
regex *regexp.Regexp // Regex to identify the supported tag names.
Foreground termbox.Attribute // Foreground color.
Background termbox.Attribute // Background color (so far always termbox.ColorDefault).
RightAligned bool // True when the string is right aligned.
tags map[string]termbox.Attribute // Tags to Termbox translation hash.
regex *regexp.Regexp // Regex to identify the supported tag names.
}
// Creates markup to define tag to Termbox translation rules and store default
// colors and column alignments.
func NewMarkup() *Markup {
markup := &Markup{}
markup.Foreground = termbox.ColorDefault
markup.Background = termbox.ColorDefault
markup.RightAligned = false
markup.Foreground = termbox.ColorDefault
markup.Background = termbox.ColorDefault
markup.RightAligned = false
markup.tags = make(map[string]termbox.Attribute)
markup.tags[`/`] = termbox.ColorDefault
markup.tags[`black`] = termbox.ColorBlack
markup.tags[`red`] = termbox.ColorRed
markup.tags[`green`] = termbox.ColorGreen
markup.tags[`yellow`] = termbox.ColorYellow
markup.tags[`blue`] = termbox.ColorBlue
markup.tags[`/`] = termbox.ColorDefault
markup.tags[`black`] = termbox.ColorBlack
markup.tags[`red`] = termbox.ColorRed
markup.tags[`green`] = termbox.ColorGreen
markup.tags[`yellow`] = termbox.ColorYellow
markup.tags[`blue`] = termbox.ColorBlue
markup.tags[`magenta`] = termbox.ColorMagenta
markup.tags[`cyan`] = termbox.ColorCyan
markup.tags[`white`] = termbox.ColorWhite
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
markup.tags[`r`] = termbox.AttrReverse
markup.regex = markup.supportedTags() // Once we have the hash we could build the regex.
markup.tags[`cyan`] = termbox.ColorCyan
markup.tags[`white`] = termbox.ColorWhite
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
markup.tags[`r`] = termbox.AttrReverse
markup.regex = markup.supportedTags() // Once we have the hash we could build the regex.
return markup
}
@ -111,17 +111,17 @@ func (markup *Markup) process(tag string, open bool) bool {
if attribute, ok := markup.tags[tag]; ok {
switch tag {
case `right`:
markup.RightAligned = open // On for <right>, off for </right>.
markup.RightAligned = open // On for <right>, off for </right>.
default:
if open {
if attribute >= termbox.AttrBold {
markup.Foreground |= attribute // Set the Termbox attribute.
markup.Foreground |= attribute // Set the Termbox attribute.
} else {
markup.Foreground = attribute // Set the Termbox color.
markup.Foreground = attribute // Set the Termbox color.
}
} else {
if attribute >= termbox.AttrBold {
markup.Foreground &= ^attribute // Clear the Termbox attribute.
markup.Foreground &= ^attribute // Clear the Termbox attribute.
} else {
markup.Foreground = termbox.ColorDefault
}
@ -138,7 +138,7 @@ func (markup *Markup) supportedTags() *regexp.Regexp {
arr := []string{}
for tag := range markup.tags {
arr = append(arr, `</?` + tag + `>`)
arr = append(arr, `</?`+tag+`>`)
}
return regexp.MustCompile(strings.Join(arr, `|`))

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -18,13 +18,13 @@ const moprc = `/.moprc`
// 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.
selectedColumn int // Stores selected column number when the column editor is active.
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.
selectedColumn int // Stores selected column number when the column editor is active.
}
// Creates the profile and attempts to load the settings from ~/.moprc file.
@ -32,13 +32,13 @@ type Profile struct {
func NewProfile() *Profile {
profile := &Profile{}
data, err := ioutil.ReadFile(profile.defaultFileName())
if err != nil { // Set default values:
profile.MarketRefresh = 12 // Market data gets fetched every 12s (5 times per minute).
profile.QuotesRefresh = 5 // Stock quotes get updated every 5s (12 times per minute).
profile.Grouped = false // Stock quotes are *not* grouped by advancing/declining.
profile.Tickers = []string{ `AAPL`, `C`, `GOOG`, `IBM`, `KO`, `ORCL`, `V` }
profile.SortColumn = 0 // Stock quotes are sorted by ticker name.
profile.Ascending = true // A to Z.
if err != nil { // Set default values:
profile.MarketRefresh = 12 // Market data gets fetched every 12s (5 times per minute).
profile.QuotesRefresh = 5 // Stock quotes get updated every 5s (12 times per minute).
profile.Grouped = false // Stock quotes are *not* grouped by advancing/declining.
profile.Tickers = []string{`AAPL`, `C`, `GOOG`, `IBM`, `KO`, `ORCL`, `V`}
profile.SortColumn = 0 // Stock quotes are sorted by ticker name.
profile.Ascending = true // A to Z.
profile.Save()
} else {
json.Unmarshal(data, profile)
@ -110,9 +110,9 @@ func (profile *Profile) RemoveTickers(tickers []string) (removed int, err error)
// for the current column, or to pick another sort column.
func (profile *Profile) Reorder() error {
if profile.selectedColumn == profile.SortColumn {
profile.Ascending = !profile.Ascending // Reverse sort order.
profile.Ascending = !profile.Ascending // Reverse sort order.
} else {
profile.SortColumn = profile.selectedColumn // Pick new sort column.
profile.SortColumn = profile.selectedColumn // Pick new sort column.
}
return profile.Save()
}

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -13,12 +13,12 @@ import (
// Screen is thin wrapper aroung Termbox library to provide basic display
// capabilities as requied by Mop.
type Screen struct {
width int // Current number of columns.
height int // Current number of rows.
cleared bool // True after the screens gets cleared.
layout *Layout // Pointer to layout (gets created by screen).
markup *Markup // Pointer to markup processor (gets created by screen).
pausedAt *time.Time // Timestamp of the pause request or nil if none.
width int // Current number of columns.
height int // Current number of rows.
cleared bool // True after the screens gets cleared.
layout *Layout // Pointer to layout (gets created by screen).
markup *Markup // Pointer to markup processor (gets created by screen).
pausedAt *time.Time // Timestamp of the pause request or nil if none.
}
// Initializes Termbox, creates screen along with layout and markup, and
@ -54,14 +54,14 @@ func (screen *Screen) Resize() *Screen {
// Pause is a toggle function that either creates a timestamp of the pause
// request or resets it to nil.
func (screen *Screen) Pause(pause bool) *Screen {
if pause {
screen.pausedAt = new(time.Time)
*screen.pausedAt = time.Now()
} else {
screen.pausedAt = nil
}
return screen
if pause {
screen.pausedAt = new(time.Time)
*screen.pausedAt = time.Now()
} else {
screen.pausedAt = nil
}
return screen
}
// Clear makes the entire screen blank using default background color.
@ -86,9 +86,9 @@ func (screen *Screen) ClearLine(x int, y int) *Screen {
// Draw accepts variable number of arguments and knows how to display the
// market data, stock quotes, current time, and an arbitrary string.
func (screen *Screen) Draw(objects ...interface{}) *Screen {
if screen.pausedAt != nil {
defer screen.DrawLine(0, 0, `<right><r>` + screen.pausedAt.Format(`3:04:05pm PST`) + `</r></right>`)
}
if screen.pausedAt != nil {
defer screen.DrawLine(0, 0, `<right><r>`+screen.pausedAt.Format(`3:04:05pm PST`)+`</r></right>`)
}
for _, ptr := range objects {
switch ptr.(type) {
case *Market:
@ -99,7 +99,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 PST`)
screen.DrawLine(0, 0, `<right>` + timestamp + `</right>`)
screen.DrawLine(0, 0, `<right>`+timestamp+`</right>`)
default:
screen.draw(ptr.(string))
}

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -14,76 +14,133 @@ import (
// setup is rather lengthy; there should probably be more concise way
// that uses reflection and avoids hardcoding the column names.
type Sorter struct {
profile *Profile // Pointer to where we store sort column and order.
profile *Profile // Pointer to where we store sort column and order.
}
type sortable []Stock
func (list sortable) Len() int { return len(list) }
func (list sortable) Len() int { return len(list) }
func (list sortable) Swap(i, j int) { list[i], list[j] = list[j], list[i] }
type byTickerAsc struct { sortable }
type byLastTradeAsc struct { sortable }
type byChangeAsc struct { sortable }
type byChangePctAsc struct { sortable }
type byOpenAsc struct { sortable }
type byLowAsc struct { sortable }
type byHighAsc struct { sortable }
type byLow52Asc struct { sortable }
type byHigh52Asc struct { sortable }
type byVolumeAsc struct { sortable }
type byAvgVolumeAsc struct { sortable }
type byPeRatioAsc struct { sortable }
type byDividendAsc struct { sortable }
type byYieldAsc struct { sortable }
type byMarketCapAsc struct { sortable }
type byTickerDesc struct { sortable }
type byLastTradeDesc struct { sortable }
type byChangeDesc struct { sortable }
type byChangePctDesc struct { sortable }
type byOpenDesc struct { sortable }
type byLowDesc struct { sortable }
type byHighDesc struct { sortable }
type byLow52Desc struct { sortable }
type byHigh52Desc struct { sortable }
type byVolumeDesc struct { sortable }
type byAvgVolumeDesc struct { sortable }
type byPeRatioDesc struct { sortable }
type byDividendDesc struct { sortable }
type byYieldDesc struct { sortable }
type byMarketCapDesc struct { sortable }
func (list byTickerAsc) Less(i, j int) bool { return list.sortable[i].Ticker < list.sortable[j].Ticker }
func (list byLastTradeAsc) Less(i, j int) bool { return list.sortable[i].LastTrade < list.sortable[j].LastTrade }
func (list byChangeAsc) Less(i, j int) bool { return c(list.sortable[i].Change) < c(list.sortable[j].Change) }
func (list byChangePctAsc) Less(i, j int) bool { return c(list.sortable[i].ChangePct) < c(list.sortable[j].ChangePct) }
func (list byOpenAsc) Less(i, j int) bool { return list.sortable[i].Open < list.sortable[j].Open }
func (list byLowAsc) Less(i, j int) bool { return list.sortable[i].Low < list.sortable[j].Low }
func (list byHighAsc) Less(i, j int) bool { return list.sortable[i].High < list.sortable[j].High }
func (list byLow52Asc) Less(i, j int) bool { return list.sortable[i].Low52 < list.sortable[j].Low52 }
func (list byHigh52Asc) Less(i, j int) bool { return list.sortable[i].High52 < list.sortable[j].High52 }
func (list byVolumeAsc) Less(i, j int) bool { return list.sortable[i].Volume < list.sortable[j].Volume }
func (list byAvgVolumeAsc) Less(i, j int) bool { return list.sortable[i].AvgVolume < list.sortable[j].AvgVolume }
func (list byPeRatioAsc) Less(i, j int) bool { return list.sortable[i].PeRatio < list.sortable[j].PeRatio }
func (list byDividendAsc) Less(i, j int) bool { return list.sortable[i].Dividend < list.sortable[j].Dividend }
func (list byYieldAsc) Less(i, j int) bool { return list.sortable[i].Yield < list.sortable[j].Yield }
func (list byMarketCapAsc) Less(i, j int) bool { return m(list.sortable[i].MarketCap) < m(list.sortable[j].MarketCap) }
func (list byTickerDesc) Less(i, j int) bool { return list.sortable[j].Ticker < list.sortable[i].Ticker }
func (list byLastTradeDesc) Less(i, j int) bool { return list.sortable[j].LastTrade < list.sortable[i].LastTrade }
func (list byChangeDesc) Less(i, j int) bool { return c(list.sortable[j].ChangePct) < c(list.sortable[i].ChangePct) }
func (list byChangePctDesc) Less(i, j int) bool { return c(list.sortable[j].ChangePct) < c(list.sortable[i].ChangePct) }
func (list byOpenDesc) Less(i, j int) bool { return list.sortable[j].Open < list.sortable[i].Open }
func (list byLowDesc) Less(i, j int) bool { return list.sortable[j].Low < list.sortable[i].Low }
func (list byHighDesc) Less(i, j int) bool { return list.sortable[j].High < list.sortable[i].High }
func (list byLow52Desc) Less(i, j int) bool { return list.sortable[j].Low52 < list.sortable[i].Low52 }
func (list byHigh52Desc) Less(i, j int) bool { return list.sortable[j].High52 < list.sortable[i].High52 }
func (list byVolumeDesc) Less(i, j int) bool { return list.sortable[j].Volume < list.sortable[i].Volume }
func (list byAvgVolumeDesc) Less(i, j int) bool { return list.sortable[j].AvgVolume < list.sortable[i].AvgVolume }
func (list byPeRatioDesc) Less(i, j int) bool { return list.sortable[j].PeRatio < list.sortable[i].PeRatio }
func (list byDividendDesc) Less(i, j int) bool { return list.sortable[j].Dividend < list.sortable[i].Dividend }
func (list byYieldDesc) Less(i, j int) bool { return list.sortable[j].Yield < list.sortable[i].Yield }
func (list byMarketCapDesc) Less(i, j int) bool { return m(list.sortable[j].MarketCap) < m(list.sortable[i].MarketCap) }
type byTickerAsc struct{ sortable }
type byLastTradeAsc struct{ sortable }
type byChangeAsc struct{ sortable }
type byChangePctAsc struct{ sortable }
type byOpenAsc struct{ sortable }
type byLowAsc struct{ sortable }
type byHighAsc struct{ sortable }
type byLow52Asc struct{ sortable }
type byHigh52Asc struct{ sortable }
type byVolumeAsc struct{ sortable }
type byAvgVolumeAsc struct{ sortable }
type byPeRatioAsc struct{ sortable }
type byDividendAsc struct{ sortable }
type byYieldAsc struct{ sortable }
type byMarketCapAsc struct{ sortable }
type byTickerDesc struct{ sortable }
type byLastTradeDesc struct{ sortable }
type byChangeDesc struct{ sortable }
type byChangePctDesc struct{ sortable }
type byOpenDesc struct{ sortable }
type byLowDesc struct{ sortable }
type byHighDesc struct{ sortable }
type byLow52Desc struct{ sortable }
type byHigh52Desc struct{ sortable }
type byVolumeDesc struct{ sortable }
type byAvgVolumeDesc struct{ sortable }
type byPeRatioDesc struct{ sortable }
type byDividendDesc struct{ sortable }
type byYieldDesc struct{ sortable }
type byMarketCapDesc struct{ sortable }
func (list byTickerAsc) Less(i, j int) bool {
return list.sortable[i].Ticker < list.sortable[j].Ticker
}
func (list byLastTradeAsc) Less(i, j int) bool {
return list.sortable[i].LastTrade < list.sortable[j].LastTrade
}
func (list byChangeAsc) Less(i, j int) bool {
return c(list.sortable[i].Change) < c(list.sortable[j].Change)
}
func (list byChangePctAsc) Less(i, j int) bool {
return c(list.sortable[i].ChangePct) < c(list.sortable[j].ChangePct)
}
func (list byOpenAsc) Less(i, j int) bool {
return list.sortable[i].Open < list.sortable[j].Open
}
func (list byLowAsc) Less(i, j int) bool { return list.sortable[i].Low < list.sortable[j].Low }
func (list byHighAsc) Less(i, j int) bool {
return list.sortable[i].High < list.sortable[j].High
}
func (list byLow52Asc) Less(i, j int) bool {
return list.sortable[i].Low52 < list.sortable[j].Low52
}
func (list byHigh52Asc) Less(i, j int) bool {
return list.sortable[i].High52 < list.sortable[j].High52
}
func (list byVolumeAsc) Less(i, j int) bool {
return list.sortable[i].Volume < list.sortable[j].Volume
}
func (list byAvgVolumeAsc) Less(i, j int) bool {
return list.sortable[i].AvgVolume < list.sortable[j].AvgVolume
}
func (list byPeRatioAsc) Less(i, j int) bool {
return list.sortable[i].PeRatio < list.sortable[j].PeRatio
}
func (list byDividendAsc) Less(i, j int) bool {
return list.sortable[i].Dividend < list.sortable[j].Dividend
}
func (list byYieldAsc) Less(i, j int) bool {
return list.sortable[i].Yield < list.sortable[j].Yield
}
func (list byMarketCapAsc) Less(i, j int) bool {
return m(list.sortable[i].MarketCap) < m(list.sortable[j].MarketCap)
}
func (list byTickerDesc) Less(i, j int) bool {
return list.sortable[j].Ticker < list.sortable[i].Ticker
}
func (list byLastTradeDesc) Less(i, j int) bool {
return list.sortable[j].LastTrade < list.sortable[i].LastTrade
}
func (list byChangeDesc) Less(i, j int) bool {
return c(list.sortable[j].ChangePct) < c(list.sortable[i].ChangePct)
}
func (list byChangePctDesc) Less(i, j int) bool {
return c(list.sortable[j].ChangePct) < c(list.sortable[i].ChangePct)
}
func (list byOpenDesc) Less(i, j int) bool {
return list.sortable[j].Open < list.sortable[i].Open
}
func (list byLowDesc) Less(i, j int) bool { return list.sortable[j].Low < list.sortable[i].Low }
func (list byHighDesc) Less(i, j int) bool {
return list.sortable[j].High < list.sortable[i].High
}
func (list byLow52Desc) Less(i, j int) bool {
return list.sortable[j].Low52 < list.sortable[i].Low52
}
func (list byHigh52Desc) Less(i, j int) bool {
return list.sortable[j].High52 < list.sortable[i].High52
}
func (list byVolumeDesc) Less(i, j int) bool {
return list.sortable[j].Volume < list.sortable[i].Volume
}
func (list byAvgVolumeDesc) Less(i, j int) bool {
return list.sortable[j].AvgVolume < list.sortable[i].AvgVolume
}
func (list byPeRatioDesc) Less(i, j int) bool {
return list.sortable[j].PeRatio < list.sortable[i].PeRatio
}
func (list byDividendDesc) Less(i, j int) bool {
return list.sortable[j].Dividend < list.sortable[i].Dividend
}
func (list byYieldDesc) Less(i, j int) bool {
return list.sortable[j].Yield < list.sortable[i].Yield
}
func (list byMarketCapDesc) Less(i, j int) bool {
return m(list.sortable[j].MarketCap) < m(list.sortable[i].MarketCap)
}
// Returns new Sorter struct.
func NewSorter(profile *Profile) *Sorter {
@ -99,39 +156,39 @@ func (sorter *Sorter) SortByCurrentColumn(stocks []Stock) *Sorter {
if sorter.profile.Ascending {
interfaces = []sort.Interface{
byTickerAsc { stocks },
byLastTradeAsc { stocks },
byChangeAsc { stocks },
byChangePctAsc { stocks },
byOpenAsc { stocks },
byLowAsc { stocks },
byHighAsc { stocks },
byLow52Asc { stocks },
byHigh52Asc { stocks },
byVolumeAsc { stocks },
byAvgVolumeAsc { stocks },
byPeRatioAsc { stocks },
byDividendAsc { stocks },
byYieldAsc { stocks },
byMarketCapAsc { stocks },
byTickerAsc{stocks},
byLastTradeAsc{stocks},
byChangeAsc{stocks},
byChangePctAsc{stocks},
byOpenAsc{stocks},
byLowAsc{stocks},
byHighAsc{stocks},
byLow52Asc{stocks},
byHigh52Asc{stocks},
byVolumeAsc{stocks},
byAvgVolumeAsc{stocks},
byPeRatioAsc{stocks},
byDividendAsc{stocks},
byYieldAsc{stocks},
byMarketCapAsc{stocks},
}
} else {
interfaces = []sort.Interface{
byTickerDesc { stocks },
byLastTradeDesc { stocks },
byChangeDesc { stocks },
byChangePctDesc { stocks },
byOpenDesc { stocks },
byLowDesc { stocks },
byHighDesc { stocks },
byLow52Desc { stocks },
byHigh52Desc { stocks },
byVolumeDesc { stocks },
byAvgVolumeDesc { stocks },
byPeRatioDesc { stocks },
byDividendDesc { stocks },
byYieldDesc { stocks },
byMarketCapDesc { stocks },
byTickerDesc{stocks},
byLastTradeDesc{stocks},
byChangeDesc{stocks},
byChangePctDesc{stocks},
byOpenDesc{stocks},
byLowDesc{stocks},
byHighDesc{stocks},
byLow52Desc{stocks},
byHigh52Desc{stocks},
byVolumeDesc{stocks},
byAvgVolumeDesc{stocks},
byPeRatioDesc{stocks},
byDividendDesc{stocks},
byYieldDesc{stocks},
byMarketCapDesc{stocks},
}
}
@ -153,7 +210,7 @@ func c(str string) float32 {
func m(str string) float32 {
multiplier := 1.0
switch str[len(str)-1:len(str)] { // Check the last character.
switch str[len(str)-1 : len(str)] { // Check the last character.
case `B`:
multiplier = 1000000000.0
case `M`:
@ -162,7 +219,7 @@ func m(str string) float32 {
multiplier = 1000.0
}
trimmed := strings.Trim(str, ` $BMK`) // Get rid of non-numeric characters.
trimmed := strings.Trim(str, ` $BMK`) // Get rid of non-numeric characters.
value, _ := strconv.ParseFloat(trimmed, 32)
return float32(value * multiplier)

@ -1,4 +1,4 @@
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
// Copyright (c) 2013-2015 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
@ -24,41 +24,41 @@ const quotesURL = `http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=sl1c1p2
// Stock stores quote information for the particular stock ticker. The data
// for all the fields except 'Advancing' is fetched using Yahoo market API.
type Stock struct {
Ticker string // Stock ticker.
LastTrade string // l1: last trade.
Change string // c6: change real time.
ChangePct string // k2: percent change real time.
Open string // o: market open price.
Low string // g: day's low.
High string // h: day's high.
Low52 string // j: 52-weeks low.
High52 string // k: 52-weeks high.
Volume string // v: volume.
AvgVolume string // a2: average volume.
PeRatio string // r2: P/E ration real time.
PeRatioX string // r: P/E ration (fallback when real time is N/A).
Dividend string // d: dividend.
Yield string // y: dividend yield.
MarketCap string // j3: market cap real time.
MarketCapX string // j1: market cap (fallback when real time is N/A).
Advancing bool // True when change is >= $0.
Ticker string // Stock ticker.
LastTrade string // l1: last trade.
Change string // c6: change real time.
ChangePct string // k2: percent change real time.
Open string // o: market open price.
Low string // g: day's low.
High string // h: day's high.
Low52 string // j: 52-weeks low.
High52 string // k: 52-weeks high.
Volume string // v: volume.
AvgVolume string // a2: average volume.
PeRatio string // r2: P/E ration real time.
PeRatioX string // r: P/E ration (fallback when real time is N/A).
Dividend string // d: dividend.
Yield string // y: dividend yield.
MarketCap string // j3: market cap real time.
MarketCapX string // j1: market cap (fallback when real time is N/A).
Advancing bool // True when change is >= $0.
}
// Quotes stores relevant pointers as well as the array of stock quotes for
// the tickers we are tracking.
type Quotes struct {
market *Market // Pointer to Market.
profile *Profile // Pointer to Profile.
stocks []Stock // Array of stock quote data.
errors string // Error string if any.
market *Market // Pointer to Market.
profile *Profile // Pointer to Profile.
stocks []Stock // Array of stock quote data.
errors string // Error string if any.
}
// Sets the initial values and returns new Quotes struct.
func NewQuotes(market *Market, profile *Profile) *Quotes {
return &Quotes{
market: market,
market: market,
profile: profile,
errors: ``,
errors: ``,
}
}
@ -102,7 +102,7 @@ func (quotes *Quotes) Ok() (bool, string) {
// when user adds new stock tickers.
func (quotes *Quotes) AddTickers(tickers []string) (added int, err error) {
if added, err = quotes.profile.AddTickers(tickers); err == nil && added > 0 {
quotes.stocks = nil // Force fetch.
quotes.stocks = nil // Force fetch.
}
return
}
@ -112,7 +112,7 @@ func (quotes *Quotes) AddTickers(tickers []string) (added int, err error) {
// when user removes existing stock tickers.
func (quotes *Quotes) RemoveTickers(tickers []string) (removed int, err error) {
if removed, err = quotes.profile.RemoveTickers(tickers); err == nil && removed > 0 {
quotes.stocks = nil // Force fetch.
quotes.stocks = nil // Force fetch.
}
return
}

Loading…
Cancel
Save