

#!/bin/bash
# the next line restarts using wish \
exec /usr/bin/wish "$0" "$@"
#
#################################
# copyright 2004 Mike Tuxford (aka moogyCode[TM])
# tuxford@earthlink.net
# irc.fdfnet.net #Linux #Groklaw
#
# stockwatch.tcl
# Relies on yahoo for stock quotes. Have fun!
#
# Right mouse click will raise a little menu for exiting
# or Ctrl-c or key-q will exit safely
#
#################################################
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; Version 2.
# (http://www.gnu.org/licenses/gpl.txt) This guarantees your
# right to use, modify, and redistribute under certain conditions.
#
#################################################
### Features along the bottom of the display ####
#
# [<-] [->] Buttons forward/backward through the stocks watched
#
# DISPLAY [] SEC is the time delay in seconds between rotating
# the displayed stock. You can change the cycle
# time by entering a number and pressing <enter>
#
# () indicator light will show GREEN while stock quotes are being
# fetched, RED when idle, and YELLOW indicates that
# one or more new stock quotes failed to be fetched.
#
# UPDATE button will fetch new quotes immediately and reset the
# fetch cycle.
#
# FETCH [] MIN is the cycle time, in minutes, between fetching new
# stock quotes. Enter a new value and press <enter> and
# it will reset the cycle to the new value.
#
# ADD/DELETE button Add or Delete stock symbols.
#
# CLOCK Your current time. :)
#
# () indicator light shows GREEN when stock market is open and RED
# when closed. This affects automated fetching of stock
# quotes but you can still always use the UPDATE button
# to fetch the last quotes when stock market is closed
#
#################################################
### NOTES on array variables ####################
# "stocks,watched" "symbol symbol"
# You can add or delete stock symbols but must notice
# that they are quoted all together as a group and not
# indiviually.
# Correct: "aaa bbb ccc"
# Incorrect: "aaa" "bbb" "ccc"
#
# "fecth,cycle" <n> Where <n> is minutes between fetching stock quotes
#
# "display,cycle" <n> Where <n> is seconds between displaying the stocks
#
# "daylight,savings" This should be set either on or off depending on
# whether NYC is currently on DST or not. This is for
# the sutomated fetching of stock qoutes to know when
# the stock market is open or closed.
#
# colors can be RGB in the form "#RRGGBB" or common color names such
# as "white", "red", etc... Just keep them in quotes
#
package require Tk
array set opt {
"stocks,watched" "SCOX NOVL IBM RHAT"
"fetch,cycle" 10
"display,cycle" 15
"daylight,savings" "off"
"gui,bg" "#000000"
"clock,bg" "#000000"
"clock,fg" "#ffd700"
"title,bg" "#000000"
"title,fg" "#ff0000"
"ticker,bg" "#000000"
"ticker,fg" "#ffd700"
"title,font" "Helvetica 12"
}
array set bmp {
"dot" "#define dot11_width 11 #define dot11_height 11
static unsigned char dot11_bits[] = {
0x00, 0x00, 0xf8, 0x00, 0xfc, 0x01, 0xfe, 0x03, 0xfe, 0x03, 0xfe, 0x03,
0xfe, 0x03, 0xfe, 0x03, 0xfc, 0x01, 0xf8, 0x00, 0x00, 0x00};"
}
set stock(fields) "symbol price change volume low high open time"
proc safe_exit {} {
foreach id [after info] {
catch {after cancel $id}
}
catch {destroy .s .}
return
}
proc update_clock {} {
## sanity check
if {![winfo exists .s]} {
safe_exit
} else {
.s.ctrl.clock configure -text [clock format [clock seconds] -format "%H:%M:%S"]
after 1000 update_clock
}
return
}
proc init_stock {s} {
global stock
foreach field $stock(fields) {
set stock($s,$field) ""
}
return
}
proc geturl_followRedirects {url args} {
array set URI [::uri::split $url] ;# Need host info from here
while {1} {
set token [eval [list http::geturl $url] $args]
if {![string match {30[1237]} [::http::ncode $token]]} {return $token}
array set meta [set ${token}(meta)]
if {![info exist meta(Location)]} {
return $token
}
array set uri [::uri::split $meta(Location)]
unset meta
if {$uri(host) == ""} { set uri(host) $URI(host) }
# problem w/ relative versus absolute paths
set url [eval ::uri::join [array get uri]]
}
}
#"SCOX",14.37,"1/30/2004","3:58pm",-0.48,15.07,15.07,14.36,87362
# ID price date time change open high low volume
proc get_stocks {} {
global opt stock
set stock(fails) ""
if {$opt(stocks,watched) == ""} {
return 0
}
bimg(market,ud) configure -foreground #00ff00
foreach symbol $opt(stocks,watched) {
http::config -useragent "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6"
if {[catch {geturl_followRedirects http://finance.yahoo.com/d/quotes.csv?s=$symbol&f=sl1d1t1c1ohgv&e=.csv -timeout 30000} tok]} {
lappend stock(fails) $symbol
} else {
# sanity check
if {[http::status $tok] == "ok"} {
set stock($symbol,raw) [split [http::data $tok] \n]
set r [split [lindex $stock($symbol,raw) 0] ","]
array set v {"symbol" 0 "price" 1 "change" 4 "volume" 8 "low" 7 "high" 6 "open" 5 "time" 3}
foreach field [array names v] {
set stock($symbol,$field) [string map {\" ""} [lindex $r $v($field)]]
}
http::cleanup $tok
} else {
http::cleanup $tok
lappend stock(fails) $symbol
}
}
}
if {[llength $stock(fails)] > 0} {
bimg(market,ud) configure -foreground #ffff00
return 0
} else {
bimg(market,ud) configure -foreground #ff0000
return 1
}
}
proc cycle_fetch {} {
global opt
catch {after cancel $opt(fetch,after)}
if {[is_market_open]} {
set result [get_stocks]
bimg(market,oc) configure -foreground #00ff00
} else {
bimg(market,oc) configure -foreground #ff0000
}
set opt(fetch,after) [after [expr {$opt(fetch,cycle)*60000}] cycle_fetch]
return
}
proc is_market_open {} {
global opt
if {[string tolower $opt(daylight,savings)] == "off"} {
set ts [expr {[clock scan "now" -base [clock seconds] -gmt 1]-18000}]
} else {
set ts [expr {[clock scan "now" -base [clock seconds] -gmt 1]-14400}]
}
set day [clock format $ts -gmt 1 -format "%w"]
if {$day == 0 || $day == 6} {
return 0
} else {
set hh [clock format $ts -gmt 1 -format "%H"]
scan $hh %d hh
set mm [clock format $ts -gmt 1 -format "%M"]
scan $mm %d mm
}
if {$hh < 9 || $hh > 15} {
return 0
}
if {$hh == 9 && $mm < 30} {
return 0
}
return 1
}
proc change_display {dir} {
global opt stock
switch -- $dir {
forward {
if {$opt(display,cur) >= [expr {[llength $opt(stocks,watched)]-1}]} {
set opt(display,cur) 0
} else {
incr opt(display,cur)
}
}
back {
if {$opt(display,cur) <= 0} {
set opt(display,cur) [expr {[llength $opt(stocks,watched)]-1}]
} else {
incr opt(display,cur) -1
}
}
default {}
}
foreach f $stock(fields) {
.s.stocks.$f configure -text $stock([lindex $opt(stocks,watched) $opt(display,cur)],$f)
}
return
}
proc cycle_display {} {
global opt
catch {after cancel $opt(display,after)}
if {[llength $opt(stocks,watched)] > 1} {
change_display "forward"
}
set opt(display,after) [after [expr {$opt(display,cycle)*1000}] cycle_display]
return
}
proc set_cycle {type} {
global opt
set w ".s.ctrl.$type"
if {![string is integer [$w get]]} {
bell
return
} else {
set opt($type,cycle) [$w get]
}
focus .s
if {$type == "display"} {
set delay [expr {$opt($type,cycle)*1000}]
} else {
set delay [expr {$opt($type,cycle)*60000}]
}
catch {after cancel $opt($type,after)}
after $delay cycle_$type
return
}
proc init_widgets {} {
global opt bmp stock
frame .s.title
foreach name $stock(fields) {
label .s.title.$name \
-bg $opt(title,bg) -foreground $opt(title,fg) \
-font $opt(title,font) -height 1 -width 9 -text $name
pack .s.title.$name -side left
}
pack .s.title -side top
frame .s.stocks
foreach field $stock(fields) {
label .s.stocks.$field \
-bg $opt(ticker,bg) -foreground $opt(ticker,fg) \
-font $opt(title,font) -height 1 -width 9 -text ""
pack .s.stocks.$field -side left
}
pack .s.stocks -side top
set b(left) "<-"
set b(right) "->"
frame .s.ctrl -bg $opt(title,bg)
foreach but {left right} {
button .s.ctrl.$but \
-activebackground #ffffff -activeforeground #0000ff \
-background #eaeaea -foreground #0000ff \
-borderwidth 1 -relief solid \
-height 1 -width 2 \
-font {Helvetica 10 bold} -text $b($but) \
-command {}
pack .s.ctrl.$but -side left -padx 3
}
.s.ctrl.left configure -command {change_display "back"}
.s.ctrl.right configure -command {change_display "forward"}
label .s.ctrl.displayHead \
-background #000000 -foreground #ffff00 \
-borderwidth 0 -relief solid \
-font {Helvetica 10} -text "DISPLAY" -height 1 -width 7
pack .s.ctrl.displayHead -side left
entry .s.ctrl.display -relief sunken -bd 1 -width 3 \
-background #ffffff -font {Helvetica 12}
pack .s.ctrl.display -side left
.s.ctrl.display insert 0 $opt(display,cycle)
bind .s.ctrl.display <Key-Return> {set_cycle "display"}
label .s.ctrl.displayTail \
-background #000000 -foreground #ffff00 \
-borderwidth 0 -relief solid \
-font {Helvetica 10} -text "SEC" -height 1 -width 4
pack .s.ctrl.displayTail -side left
image create bitmap bimg(market,ud) -data $bmp(dot)
bimg(market,ud) configure -foreground #ffff00
label .s.ctrl.ud \
-background #000000 -foreground #000000 \
-borderwidth 1 -relief solid \
-image bimg(market,ud)
pack .s.ctrl.ud -side left
button .s.ctrl.update \
-activebackground #ffffff -activeforeground #0000ff \
-background #eaeaea -foreground #0000ff \
-borderwidth 1 -relief solid \
-font {Helvetica 10} -text "UPDATE" -height 1 -width 6 \
-command {get_stocks}
pack .s.ctrl.update -side left -padx 5
label .s.ctrl.fetchHead \
-background #000000 -foreground #ffff00 \
-borderwidth 0 -relief solid \
-font {Helvetica 10} -text "FETCH" -height 1 -width 6
pack .s.ctrl.fetchHead -side left
entry .s.ctrl.fetch -relief sunken -bd 1 -width 2 \
-background #ffffff -font {Helvetica 12}
pack .s.ctrl.fetch -side left
.s.ctrl.fetch insert 0 $opt(fetch,cycle)
bind .s.ctrl.fetch <Key-Return> {set_cycle "fetch"}
label .s.ctrl.fetchTail \
-background #000000 -foreground #ffff00 \
-borderwidth 0 -relief solid \
-font {Helvetica 10} -text "MIN" -height 1 -width 4
pack .s.ctrl.fetchTail -side left
button .s.ctrl.edit \
-activebackground #ffffff -activeforeground #0000ff \
-background #eaeaea -foreground #0000ff \
-borderwidth 1 -relief solid -font {Helvetica 10} \
-text "ADD/DEL" -height 1 -width 8 -command {pop_win "edit" "Add/Del Stocks"}
pack .s.ctrl.edit -side left -padx 5
image create bitmap bimg(market,oc) -data $bmp(dot)
bimg(market,oc) configure -foreground #ff0000
label .s.ctrl.market \
-background #000000 -foreground #000000 \
-borderwidth 1 -relief solid \
-image bimg(market,oc)
pack .s.ctrl.market -side right -padx 5
label .s.ctrl.clock \
-bg $opt(clock,bg) -foreground $opt(clock,fg) \
-font {Helvetica 14} -text "00:00:00" -height 1 -width 8
pack .s.ctrl.clock -side right
pack .s.ctrl -side top -anchor w -fill x
set opt(display,cur) 0
return
}
proc edit_stock {t} {
global opt
set w .edit
focus $w
if {[$w.$t.$t get] == ""} {
focus $w.$t.$t
pop_win "error" "Illegal value!"
return
} else {
set sym [string trim [string toupper [$w.$t.$t get]]]
$w.$t.$t delete 0 end
}
switch -- $t {
"add" {
if {[lsearch $sym $opt(stocks,watched)] != -1} {
focus $w.add.add
pop_win "error" "$sym already being watched"
return
}
if {[string first " " $sym] != -1} {
focus $w.add.add
pop_win "error" "Can only add one stock at a time."
return
}
pop_win notice "$sym added to stocks watched"
set result [init_stock $sym]
lappend opt(stocks,watched) $sym
set result [get_stocks]
}
"delete" {
if {[lsearch -exact $opt(stocks,watched) $sym] == -1} {
focus $w.add.add
pop_win "error" "I don't see \"$sym\" among the stocks watched."
return
} else {
set idx [lsearch -exact $opt(stocks,watched) $sym]
set opt(stocks,watched) [lreplace $opt(stocks,watched) $idx $idx]
catch {after cancel $opt(display,after)}
set opt(display,cur) 0
cycle_display
}
}
default {}
}
return
}
array set win {
"about,x" 220 "about,y" 100
"about,bg" "#0000ff"
"edit,bg" "#000000"
"error,bg" "#eaeaea"
"notice,bg" "#eaeaea"
}
proc pop_win {n txt} {
global opt win
set w .$n
# Abort if window already exists
if {[winfo exists $w]} {
raise $w
focus $w
return
}
toplevel $w
wm deiconify $w
$w configure -background $win($n,bg)
$w configure -cursor draft_small
switch -- $n {
"about" {
wm geometry $w $win($n,x)x$win($n,y)+[expr {[winfo x .s]+50}]+[winfo y .s]
wm title $w $txt
label $w.title \
-bg #0000ff -foreground #ffffff \
-font {Helvetica 14 bold} -height 3 -width 30 \
-text "Stock Watch was written by\nmoogyCode\[TM\] 2004\nMike Tuxford"
pack $w.title -side top
button $w.close \
-activebackground #ffffff -activeforeground #0000ff \
-background #eaeaea -foreground #0000ff \
-borderwidth 1 -relief solid -height 1 -width 20 \
-font {Helvetica 14} -text "I knew that!" -command {destroy .about}
pack $w.close -side top
bind $w <Control-c> "destroy $w"
}
"edit" {
wm geometry $w +[expr {[winfo x .s]+50}]+[winfo y .s]
wm title $w $txt
frame $w.add -relief groove -bd 5
label $w.add.head \
-background #eaeaea -foreground #000000 \
-borderwidth 0 -relief flat \
-font {Helvetica 12} -text "Add a Stock" -height 1 -width 15
pack $w.add.head -side left -padx 10 -pady 5
entry $w.add.add -relief sunken -bd 3 -width 6 \
-background #ffffff -font {Helvetica 12}
pack $w.add.add -side left -padx 10 -pady 5
bind $w.add.add <Key-Return> {edit_stock "add"}
pack $w.add -side top -padx 10 -pady 5
frame $w.delete -relief groove -bd 5
label $w.delete.head \
-background #eaeaea -foreground #000000 \
-borderwidth 0 -relief flat \
-font {Helvetica 12} -text "Delete a Stock" -height 1 -width 15
pack $w.delete.head -side left -padx 10 -pady 5
entry $w.delete.delete -relief sunken -bd 3 -width 6 \
-background #ffffff -font {Helvetica 12}
pack $w.delete.delete -side left -padx 10 -pady 5
bind $w.delete.delete <Key-Return> {edit_stock "delete"}
pack $w.delete -side top -padx 10
button $w.close \
-activebackground #ffffff -activeforeground #0000ff \
-background #eaeaea -foreground #0000ff \
-borderwidth 1 -relief solid -height 1 -width 26 \
-font {Helvetica 12} -text "CLOSE WINDOW" -command "destroy .$n"
pack $w.close -side bottom -pady 5
bind $w <Control-c> "destroy $w"
}
error - notice {
wm geometry $w +[expr {[winfo x .s]+50}]+[winfo y .s]
wm title $w "Error"
label $w.msg \
-background #eaeaea -foreground #000000 \
-borderwidth 0 -relief flat -font {Helvetica 12} \
-text $txt -width [expr {[string length $txt]+2}]
pack $w.msg -side top -padx 10 -pady 5
button $w.close \
-activebackground #ffffff -activeforeground #0000ff \
-background #eaeaea -foreground #0000ff \
-borderwidth 1 -relief solid -height 1 -width 4 \
-font {Helvetica 12} -text "OK" -command "destroy $w"
pack $w.close -side bottom -pady 5
bind $w <Control-c> "destroy $w"
}
default { destroy $w }
}
return
}
proc GUI {} {
global opt
wm withdraw .
set w .s
toplevel $w
$w configure -background $opt(gui,bg)
wm title $w "Stock Watcher"
wm deiconify $w
#####################
# MAIN MENU
menu $w.main -tearoff 0
$w.main configure -font {Helvetica 10}
set ma $w.main.file
menu $ma -tearoff 0
$ma configure -font {Helvetica 10}
$w.main add cascade -label "Menu" -menu $ma
$ma add separator
$ma add command -label "About" -command {pop_win "about" "About"}
$ma add command -label "Exit" -command {safe_exit}
$ma add separator
$w configure -cursor draft_small
#
# END MAIN MENU ###
####################
# WIDGETS
init_widgets
####################
# bind some Hotkeys
bind $w <Control-c> {safe_exit}
bind $w <Key-q> {safe_exit}
bind $w <ButtonPress-3> {tk_popup .s.main [expr [winfo pointerx .s] -5] [expr [winfo pointery .s] -5]}
### End GUI ##########
}
####################
# Get the show started
package require http
package require uri
GUI
update_clock
#devp_init
foreach stk $opt(stocks,watched) {
init_stock $stk
}
foreach f $stock(fields) {
.s.stocks.$f configure -text $stock([lindex $opt(stocks,watched) $opt(display,cur)],$f)
}
if {[is_market_open]} {
bimg(market,oc) configure -foreground #00ff00
} else {
bimg(market,oc) configure -foreground #ff0000
get_stocks
}
cycle_fetch
change_display forward
set opt(display,after) [after [expr {$opt(display,cycle)*1000}] cycle_display]Feb 27, 2004: Added copyright and GPL notice at the request of a groklaw member. March 4, 2004 Minor bug fix.
See also Tcl Ticker

