diff --git a/README.md b/README.md index 2f49fc5..5e37013 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ For demonstration purposes Mop comes preconfigured with a number of stock ticker g Group stocks by advancing/declining issues. f Set a filtering expression. F Unset a filtering expression. + PgDn Scroll Down, down arrow key also works. + PgUp Scroll up, up arrow key also works. ? Display help screen. esc Quit mop. diff --git a/cmd/mop/main.go b/cmd/mop/main.go index 0e7e8e8..4636339 100644 --- a/cmd/mop/main.go +++ b/cmd/mop/main.go @@ -33,6 +33,8 @@ NO WARRANTIES OF ANY KIND WHATSOEVER. SEE THE LICENSE FILE FOR DETAILS. g Group stocks by advancing/declining issues. o Change column sort order. p Pause market data and stock updates. + Scroll Scroll up/down. + PgUp/PgDn; Up/Down arrow; j/k;J/K also all scroll up/down q Quit mop. esc Ditto. @@ -46,12 +48,19 @@ func mainLoop(screen *mop.Screen, profile *mop.Profile) { var lineEditor *mop.LineEditor var columnEditor *mop.ColumnEditor - keyboardQueue := make(chan termbox.Event) + termbox.SetInputMode(termbox.InputMouse) + + // use buffered channel for keyboard event queue + keyboardQueue := make(chan termbox.Event, 128) + timestampQueue := time.NewTicker(1 * time.Second) quotesQueue := time.NewTicker(5 * time.Second) marketQueue := time.NewTicker(12 * time.Second) showingHelp := false paused := false + upDownJump := profile.UpDownJump + redrawQuotesFlag := false + redrawMarketFlag := false go func() { for { @@ -61,7 +70,8 @@ func mainLoop(screen *mop.Screen, profile *mop.Profile) { market := mop.NewMarket() quotes := mop.NewQuotes(market, profile) - screen.Draw(market, quotes) + screen.Draw(market) + screen.Draw(quotes) loop: for { @@ -92,6 +102,26 @@ loop: } else if event.Ch == '?' || event.Ch == 'h' || event.Ch == 'H' { showingHelp = true screen.Clear().Draw(help) + } else if event.Key == termbox.KeyPgdn || + event.Ch == 'J' { + screen.IncreaseOffset(upDownJump) + redrawQuotesFlag = true + } else if event.Key == termbox.KeyPgup || + event.Ch == 'K' { + screen.DecreaseOffset(upDownJump) + redrawQuotesFlag = true + } else if event.Key == termbox.KeyArrowUp || event.Ch == 'k' { + screen.DecreaseOffset(1) + redrawQuotesFlag = true + } else if event.Key == termbox.KeyArrowDown || event.Ch == 'j' { + screen.IncreaseOffset(1) + redrawQuotesFlag = true + } else if event.Key == termbox.KeyHome { + screen.ScrollTop() + redrawQuotesFlag = true + } else if event.Key == termbox.KeyEnd { + screen.ScrollBottom() + redrawQuotesFlag = true } } else if lineEditor != nil { if done := lineEditor.Handle(event); done { @@ -108,10 +138,26 @@ loop: case termbox.EventResize: screen.Resize() if !showingHelp { - screen.Draw(market, quotes) + //screen.Draw(market) + //redrawQuotesFlag = true + //screen.Draw(market) + redrawQuotesFlag = true + redrawMarketFlag = true + //screen.DrawOldQuotes(quotes) } else { screen.Draw(help) } + case termbox.EventMouse: + if lineEditor == nil && columnEditor == nil && !showingHelp { + switch event.Key { + case termbox.MouseWheelUp: + screen.DecreaseOffset(5) + redrawQuotesFlag = true + case termbox.MouseWheelDown: + screen.IncreaseOffset(5) + redrawQuotesFlag = true + } + } } case <-timestampQueue.C: @@ -120,8 +166,9 @@ loop: } case <-quotesQueue.C: - if !showingHelp && !paused { - screen.Draw(quotes) + if !showingHelp && !paused && len(keyboardQueue) == 0 { + go quotes.Fetch() + redrawQuotesFlag = true } case <-marketQueue.C: @@ -129,6 +176,15 @@ loop: screen.Draw(market) } } + + if redrawQuotesFlag && len(keyboardQueue) == 0 { + screen.DrawOldQuotes(quotes) + redrawQuotesFlag = false + } + if redrawMarketFlag && len(keyboardQueue) == 0 { + screen.Draw(market) + redrawMarketFlag = false + } } } diff --git a/profile.go b/profile.go index 93bbfaa..b3d92c1 100644 --- a/profile.go +++ b/profile.go @@ -31,6 +31,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 + UpDownJump int // Number of lines to go up/down when scrolling. Colors struct { // User defined colors Gain string Loss string @@ -93,6 +94,10 @@ func NewProfile(filename string) (*Profile, error) { } profile.selectedColumn = -1 + if profile.UpDownJump < 1 { + profile.UpDownJump = 10 + } + return profile, err } @@ -105,6 +110,7 @@ func (profile *Profile) InitDefaultProfile() { profile.SortColumn = 0 // Stock quotes are sorted by ticker name. profile.Ascending = true // A to Z. profile.Filter = "" + profile.UpDownJump = 10 profile.Colors.Gain = defaultGainColor profile.Colors.Loss = defaultLossColor profile.Colors.Tag = defaultTagColor diff --git a/screen.go b/screen.go index 9a9b45d..23a68bb 100644 --- a/screen.go +++ b/screen.go @@ -16,12 +16,15 @@ import ( // 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. - 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. + offset int // Offset for scolling + headerLine int // Line number of header for scroll feature + max int // highest offset } // Initializes Termbox, creates screen along with layout and markup, and @@ -34,6 +37,7 @@ func NewScreen(profile *Profile) *Screen { screen := &Screen{} screen.layout = NewLayout() screen.markup = NewMarkup(profile) + screen.offset = 0 return screen.Resize() } @@ -86,6 +90,45 @@ func (screen *Screen) ClearLine(x int, y int) *Screen { return screen } +// Increase the offset for scrolling feature by n +// Takes number of tickers as max, so not scrolling down forever +func (screen *Screen) IncreaseOffset(n int) { + if screen.offset+n <= screen.max { + screen.offset += n + } else if screen.max > screen.height { + screen.offset = screen.max + } +} + +// Decrease the offset for scrolling feature by n +func (screen *Screen) DecreaseOffset(n int) { + if screen.offset > n { + screen.offset -= n + } else { + screen.offset = 0 + } +} + +func (screen *Screen) ScrollTop() { + screen.offset = 0 +} + +func (screen *Screen) ScrollBottom() { + if screen.max > screen.height { + screen.offset = screen.max + } +} + +func (screen *Screen) DrawOldQuotes(quotes *Quotes) { + screen.draw(screen.layout.Quotes(quotes), true) + termbox.Flush() +} + +func (screen *Screen) DrawOldMarket(market *Market) { + screen.draw(screen.layout.Market(market), false) + termbox.Flush() +} + // 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 { @@ -97,24 +140,34 @@ func (screen *Screen) Draw(objects ...interface{}) *Screen { switch ptr.(type) { case *Market: object := ptr.(*Market) - screen.draw(screen.layout.Market(object.Fetch())) + screen.draw(screen.layout.Market(object.Fetch()), false) case *Quotes: object := ptr.(*Quotes) - screen.draw(screen.layout.Quotes(object.Fetch())) + screen.draw(screen.layout.Quotes(object.Fetch()), true) case time.Time: timestamp := ptr.(time.Time).Format(`3:04:05pm ` + zonename) screen.DrawLine(0, 0, ``) default: - screen.draw(ptr.(string)) + screen.draw(ptr.(string), false) } } + termbox.Flush() + return screen } // DrawLine takes the incoming string, tokenizes it to extract markup // elements, and displays it all starting at (x,y) location. + +// DrawLineFlush gives the option to flush screen after drawing + +// wrapper for DrawLineFlush func (screen *Screen) DrawLine(x int, y int, str string) { + screen.DrawLineFlush(x, y, str, true) +} + +func (screen *Screen) DrawLineFlush(x int, y int, str string, flush bool) { start, column := 0, 0 for _, token := range screen.markup.Tokenize(str) { @@ -134,31 +187,57 @@ func (screen *Screen) DrawLine(x int, y int, str string) { termbox.SetCell(start, y, char, screen.markup.Foreground, screen.markup.Background) } } - termbox.Flush() + if flush { + termbox.Flush() + } } // Underlying workhorse function that takes multiline string, splits it into // lines, and displays them row by row. -func (screen *Screen) draw(str string) { +func (screen *Screen) draw(str string, offset bool) { if !screen.cleared { screen.Clear() } var allLines []string drewHeading := false + screen.width, screen.height = termbox.Size() + tempFormat := "%" + strconv.Itoa(screen.width) + "s" blankLine := fmt.Sprintf(tempFormat, "") allLines = strings.Split(str, "\n") + if offset { + screen.max = len(allLines) - screen.height + screen.headerLine + } + // Write the lines being updated. for row := 0; row < len(allLines); row++ { - 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") { - drewHeading = true + if offset { + // Did we draw the underlined heading row? This is a crude + // check, but--see comments below... + // --- Heading row only appears for quotes, so offset is true + if !drewHeading { + if strings.Contains(allLines[row], "Ticker") && + strings.Contains(allLines[row], "Last") && + strings.Contains(allLines[row], "Change") { + drewHeading = true + screen.headerLine = row + screen.DrawLine(0, row, allLines[row]) + // move on to the point to offset to + row += screen.offset + } + } else { + // only write the necessary lines + if row <= len(allLines) && + row > screen.headerLine { + screen.DrawLineFlush(0, row-screen.offset, allLines[row], false) + } else if row > len(allLines) + 1 { + row = len(allLines) + } + } + } else { + screen.DrawLineFlush(0, row, allLines[row], false) } } // If the quotes lines in this cycle are shorter than in the previous @@ -174,8 +253,10 @@ 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++ { - screen.DrawLine(0, i, blankLine) + for i := len(allLines) - 1 - screen.offset; i < screen.height; i++ { + if i > screen.headerLine { + screen.DrawLine(0, i, blankLine) + } } } }