├── .DS_Store ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── images └── rust-stock.gif └── src ├── aio.rs ├── events.rs ├── lib.rs ├── main.rs ├── stock.rs └── widget.rs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangbqsky/rust-stock/53cf0ffa25c4ff503a964c6119d207f7176ff204/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android_system_properties" 7 | version = "0.1.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 10 | dependencies = [ 11 | "libc", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "bumpalo" 28 | version = "3.12.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 31 | 32 | [[package]] 33 | name = "cassowary" 34 | version = "0.3.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 37 | 38 | [[package]] 39 | name = "cc" 40 | version = "1.0.79" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "chrono" 52 | version = "0.4.23" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" 55 | dependencies = [ 56 | "iana-time-zone", 57 | "js-sys", 58 | "num-integer", 59 | "num-traits", 60 | "time", 61 | "wasm-bindgen", 62 | "winapi", 63 | ] 64 | 65 | [[package]] 66 | name = "codespan-reporting" 67 | version = "0.11.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 70 | dependencies = [ 71 | "termcolor", 72 | "unicode-width", 73 | ] 74 | 75 | [[package]] 76 | name = "core-foundation" 77 | version = "0.9.3" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 80 | dependencies = [ 81 | "core-foundation-sys", 82 | "libc", 83 | ] 84 | 85 | [[package]] 86 | name = "core-foundation-sys" 87 | version = "0.8.3" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 90 | 91 | [[package]] 92 | name = "crossterm" 93 | version = "0.25.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" 96 | dependencies = [ 97 | "bitflags", 98 | "crossterm_winapi", 99 | "libc", 100 | "mio", 101 | "parking_lot", 102 | "serde", 103 | "signal-hook", 104 | "signal-hook-mio", 105 | "winapi", 106 | ] 107 | 108 | [[package]] 109 | name = "crossterm_winapi" 110 | version = "0.9.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" 113 | dependencies = [ 114 | "winapi", 115 | ] 116 | 117 | [[package]] 118 | name = "cxx" 119 | version = "1.0.92" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72" 122 | dependencies = [ 123 | "cc", 124 | "cxxbridge-flags", 125 | "cxxbridge-macro", 126 | "link-cplusplus", 127 | ] 128 | 129 | [[package]] 130 | name = "cxx-build" 131 | version = "1.0.92" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613" 134 | dependencies = [ 135 | "cc", 136 | "codespan-reporting", 137 | "once_cell", 138 | "proc-macro2", 139 | "quote", 140 | "scratch", 141 | "syn", 142 | ] 143 | 144 | [[package]] 145 | name = "cxxbridge-flags" 146 | version = "1.0.92" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97" 149 | 150 | [[package]] 151 | name = "cxxbridge-macro" 152 | version = "1.0.92" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56" 155 | dependencies = [ 156 | "proc-macro2", 157 | "quote", 158 | "syn", 159 | ] 160 | 161 | [[package]] 162 | name = "dirs-next" 163 | version = "2.0.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 166 | dependencies = [ 167 | "cfg-if", 168 | "dirs-sys-next", 169 | ] 170 | 171 | [[package]] 172 | name = "dirs-sys-next" 173 | version = "0.1.2" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 176 | dependencies = [ 177 | "libc", 178 | "redox_users", 179 | "winapi", 180 | ] 181 | 182 | [[package]] 183 | name = "errno" 184 | version = "0.2.8" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 187 | dependencies = [ 188 | "errno-dragonfly", 189 | "libc", 190 | "winapi", 191 | ] 192 | 193 | [[package]] 194 | name = "errno-dragonfly" 195 | version = "0.1.2" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 198 | dependencies = [ 199 | "cc", 200 | "libc", 201 | ] 202 | 203 | [[package]] 204 | name = "fastrand" 205 | version = "1.9.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 208 | dependencies = [ 209 | "instant", 210 | ] 211 | 212 | [[package]] 213 | name = "foreign-types" 214 | version = "0.3.2" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 217 | dependencies = [ 218 | "foreign-types-shared", 219 | ] 220 | 221 | [[package]] 222 | name = "foreign-types-shared" 223 | version = "0.1.1" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 226 | 227 | [[package]] 228 | name = "getrandom" 229 | version = "0.2.8" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 232 | dependencies = [ 233 | "cfg-if", 234 | "libc", 235 | "wasi 0.11.0+wasi-snapshot-preview1", 236 | ] 237 | 238 | [[package]] 239 | name = "http_req" 240 | version = "0.9.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "ba2b47a95070b135fd0e17a307ab18af91e15d4df99cc59e046fafb904404926" 243 | dependencies = [ 244 | "native-tls", 245 | "unicase", 246 | ] 247 | 248 | [[package]] 249 | name = "iana-time-zone" 250 | version = "0.1.53" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 253 | dependencies = [ 254 | "android_system_properties", 255 | "core-foundation-sys", 256 | "iana-time-zone-haiku", 257 | "js-sys", 258 | "wasm-bindgen", 259 | "winapi", 260 | ] 261 | 262 | [[package]] 263 | name = "iana-time-zone-haiku" 264 | version = "0.1.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 267 | dependencies = [ 268 | "cxx", 269 | "cxx-build", 270 | ] 271 | 272 | [[package]] 273 | name = "instant" 274 | version = "0.1.12" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 277 | dependencies = [ 278 | "cfg-if", 279 | ] 280 | 281 | [[package]] 282 | name = "io-lifetimes" 283 | version = "1.0.6" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" 286 | dependencies = [ 287 | "libc", 288 | "windows-sys 0.45.0", 289 | ] 290 | 291 | [[package]] 292 | name = "itoa" 293 | version = "1.0.6" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 296 | 297 | [[package]] 298 | name = "js-sys" 299 | version = "0.3.61" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 302 | dependencies = [ 303 | "wasm-bindgen", 304 | ] 305 | 306 | [[package]] 307 | name = "lazy_static" 308 | version = "1.4.0" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 311 | 312 | [[package]] 313 | name = "libc" 314 | version = "0.2.140" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 317 | 318 | [[package]] 319 | name = "link-cplusplus" 320 | version = "1.0.8" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 323 | dependencies = [ 324 | "cc", 325 | ] 326 | 327 | [[package]] 328 | name = "linux-raw-sys" 329 | version = "0.1.4" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 332 | 333 | [[package]] 334 | name = "lock_api" 335 | version = "0.4.9" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 338 | dependencies = [ 339 | "autocfg", 340 | "scopeguard", 341 | ] 342 | 343 | [[package]] 344 | name = "log" 345 | version = "0.4.17" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 348 | dependencies = [ 349 | "cfg-if", 350 | ] 351 | 352 | [[package]] 353 | name = "mio" 354 | version = "0.8.6" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" 357 | dependencies = [ 358 | "libc", 359 | "log", 360 | "wasi 0.11.0+wasi-snapshot-preview1", 361 | "windows-sys 0.45.0", 362 | ] 363 | 364 | [[package]] 365 | name = "native-tls" 366 | version = "0.2.11" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 369 | dependencies = [ 370 | "lazy_static", 371 | "libc", 372 | "log", 373 | "openssl", 374 | "openssl-probe", 375 | "openssl-sys", 376 | "schannel", 377 | "security-framework", 378 | "security-framework-sys", 379 | "tempfile", 380 | ] 381 | 382 | [[package]] 383 | name = "num-integer" 384 | version = "0.1.45" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 387 | dependencies = [ 388 | "autocfg", 389 | "num-traits", 390 | ] 391 | 392 | [[package]] 393 | name = "num-traits" 394 | version = "0.2.15" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 397 | dependencies = [ 398 | "autocfg", 399 | ] 400 | 401 | [[package]] 402 | name = "once_cell" 403 | version = "1.17.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 406 | 407 | [[package]] 408 | name = "openssl" 409 | version = "0.10.45" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" 412 | dependencies = [ 413 | "bitflags", 414 | "cfg-if", 415 | "foreign-types", 416 | "libc", 417 | "once_cell", 418 | "openssl-macros", 419 | "openssl-sys", 420 | ] 421 | 422 | [[package]] 423 | name = "openssl-macros" 424 | version = "0.1.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 427 | dependencies = [ 428 | "proc-macro2", 429 | "quote", 430 | "syn", 431 | ] 432 | 433 | [[package]] 434 | name = "openssl-probe" 435 | version = "0.1.5" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 438 | 439 | [[package]] 440 | name = "openssl-sys" 441 | version = "0.9.80" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" 444 | dependencies = [ 445 | "autocfg", 446 | "cc", 447 | "libc", 448 | "pkg-config", 449 | "vcpkg", 450 | ] 451 | 452 | [[package]] 453 | name = "parking_lot" 454 | version = "0.12.1" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 457 | dependencies = [ 458 | "lock_api", 459 | "parking_lot_core", 460 | ] 461 | 462 | [[package]] 463 | name = "parking_lot_core" 464 | version = "0.9.7" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 467 | dependencies = [ 468 | "cfg-if", 469 | "libc", 470 | "redox_syscall", 471 | "smallvec", 472 | "windows-sys 0.45.0", 473 | ] 474 | 475 | [[package]] 476 | name = "pkg-config" 477 | version = "0.3.26" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 480 | 481 | [[package]] 482 | name = "proc-macro2" 483 | version = "1.0.51" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 486 | dependencies = [ 487 | "unicode-ident", 488 | ] 489 | 490 | [[package]] 491 | name = "quote" 492 | version = "1.0.23" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 495 | dependencies = [ 496 | "proc-macro2", 497 | ] 498 | 499 | [[package]] 500 | name = "redox_syscall" 501 | version = "0.2.16" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 504 | dependencies = [ 505 | "bitflags", 506 | ] 507 | 508 | [[package]] 509 | name = "redox_users" 510 | version = "0.4.3" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 513 | dependencies = [ 514 | "getrandom", 515 | "redox_syscall", 516 | "thiserror", 517 | ] 518 | 519 | [[package]] 520 | name = "rust-stock" 521 | version = "1.1.0" 522 | dependencies = [ 523 | "chrono", 524 | "crossterm", 525 | "dirs-next", 526 | "http_req", 527 | "serde", 528 | "serde_json", 529 | "tui", 530 | "unicode-width", 531 | ] 532 | 533 | [[package]] 534 | name = "rustix" 535 | version = "0.36.9" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" 538 | dependencies = [ 539 | "bitflags", 540 | "errno", 541 | "io-lifetimes", 542 | "libc", 543 | "linux-raw-sys", 544 | "windows-sys 0.45.0", 545 | ] 546 | 547 | [[package]] 548 | name = "ryu" 549 | version = "1.0.13" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 552 | 553 | [[package]] 554 | name = "schannel" 555 | version = "0.1.21" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 558 | dependencies = [ 559 | "windows-sys 0.42.0", 560 | ] 561 | 562 | [[package]] 563 | name = "scopeguard" 564 | version = "1.1.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 567 | 568 | [[package]] 569 | name = "scratch" 570 | version = "1.0.5" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" 573 | 574 | [[package]] 575 | name = "security-framework" 576 | version = "2.8.2" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" 579 | dependencies = [ 580 | "bitflags", 581 | "core-foundation", 582 | "core-foundation-sys", 583 | "libc", 584 | "security-framework-sys", 585 | ] 586 | 587 | [[package]] 588 | name = "security-framework-sys" 589 | version = "2.8.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" 592 | dependencies = [ 593 | "core-foundation-sys", 594 | "libc", 595 | ] 596 | 597 | [[package]] 598 | name = "serde" 599 | version = "1.0.154" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" 602 | dependencies = [ 603 | "serde_derive", 604 | ] 605 | 606 | [[package]] 607 | name = "serde_derive" 608 | version = "1.0.154" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" 611 | dependencies = [ 612 | "proc-macro2", 613 | "quote", 614 | "syn", 615 | ] 616 | 617 | [[package]] 618 | name = "serde_json" 619 | version = "1.0.94" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" 622 | dependencies = [ 623 | "itoa", 624 | "ryu", 625 | "serde", 626 | ] 627 | 628 | [[package]] 629 | name = "signal-hook" 630 | version = "0.3.15" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" 633 | dependencies = [ 634 | "libc", 635 | "signal-hook-registry", 636 | ] 637 | 638 | [[package]] 639 | name = "signal-hook-mio" 640 | version = "0.2.3" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 643 | dependencies = [ 644 | "libc", 645 | "mio", 646 | "signal-hook", 647 | ] 648 | 649 | [[package]] 650 | name = "signal-hook-registry" 651 | version = "1.4.1" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 654 | dependencies = [ 655 | "libc", 656 | ] 657 | 658 | [[package]] 659 | name = "smallvec" 660 | version = "1.10.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 663 | 664 | [[package]] 665 | name = "syn" 666 | version = "1.0.109" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 669 | dependencies = [ 670 | "proc-macro2", 671 | "quote", 672 | "unicode-ident", 673 | ] 674 | 675 | [[package]] 676 | name = "tempfile" 677 | version = "3.4.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 680 | dependencies = [ 681 | "cfg-if", 682 | "fastrand", 683 | "redox_syscall", 684 | "rustix", 685 | "windows-sys 0.42.0", 686 | ] 687 | 688 | [[package]] 689 | name = "termcolor" 690 | version = "1.2.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 693 | dependencies = [ 694 | "winapi-util", 695 | ] 696 | 697 | [[package]] 698 | name = "thiserror" 699 | version = "1.0.39" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" 702 | dependencies = [ 703 | "thiserror-impl", 704 | ] 705 | 706 | [[package]] 707 | name = "thiserror-impl" 708 | version = "1.0.39" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" 711 | dependencies = [ 712 | "proc-macro2", 713 | "quote", 714 | "syn", 715 | ] 716 | 717 | [[package]] 718 | name = "time" 719 | version = "0.1.45" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 722 | dependencies = [ 723 | "libc", 724 | "wasi 0.10.0+wasi-snapshot-preview1", 725 | "winapi", 726 | ] 727 | 728 | [[package]] 729 | name = "tui" 730 | version = "0.19.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" 733 | dependencies = [ 734 | "bitflags", 735 | "cassowary", 736 | "crossterm", 737 | "serde", 738 | "unicode-segmentation", 739 | "unicode-width", 740 | ] 741 | 742 | [[package]] 743 | name = "unicase" 744 | version = "2.6.0" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 747 | dependencies = [ 748 | "version_check", 749 | ] 750 | 751 | [[package]] 752 | name = "unicode-ident" 753 | version = "1.0.8" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 756 | 757 | [[package]] 758 | name = "unicode-segmentation" 759 | version = "1.10.1" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 762 | 763 | [[package]] 764 | name = "unicode-width" 765 | version = "0.1.10" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 768 | 769 | [[package]] 770 | name = "vcpkg" 771 | version = "0.2.15" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 774 | 775 | [[package]] 776 | name = "version_check" 777 | version = "0.9.4" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 780 | 781 | [[package]] 782 | name = "wasi" 783 | version = "0.10.0+wasi-snapshot-preview1" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 786 | 787 | [[package]] 788 | name = "wasi" 789 | version = "0.11.0+wasi-snapshot-preview1" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 792 | 793 | [[package]] 794 | name = "wasm-bindgen" 795 | version = "0.2.84" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 798 | dependencies = [ 799 | "cfg-if", 800 | "wasm-bindgen-macro", 801 | ] 802 | 803 | [[package]] 804 | name = "wasm-bindgen-backend" 805 | version = "0.2.84" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 808 | dependencies = [ 809 | "bumpalo", 810 | "log", 811 | "once_cell", 812 | "proc-macro2", 813 | "quote", 814 | "syn", 815 | "wasm-bindgen-shared", 816 | ] 817 | 818 | [[package]] 819 | name = "wasm-bindgen-macro" 820 | version = "0.2.84" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 823 | dependencies = [ 824 | "quote", 825 | "wasm-bindgen-macro-support", 826 | ] 827 | 828 | [[package]] 829 | name = "wasm-bindgen-macro-support" 830 | version = "0.2.84" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 833 | dependencies = [ 834 | "proc-macro2", 835 | "quote", 836 | "syn", 837 | "wasm-bindgen-backend", 838 | "wasm-bindgen-shared", 839 | ] 840 | 841 | [[package]] 842 | name = "wasm-bindgen-shared" 843 | version = "0.2.84" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 846 | 847 | [[package]] 848 | name = "winapi" 849 | version = "0.3.9" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 852 | dependencies = [ 853 | "winapi-i686-pc-windows-gnu", 854 | "winapi-x86_64-pc-windows-gnu", 855 | ] 856 | 857 | [[package]] 858 | name = "winapi-i686-pc-windows-gnu" 859 | version = "0.4.0" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 862 | 863 | [[package]] 864 | name = "winapi-util" 865 | version = "0.1.5" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 868 | dependencies = [ 869 | "winapi", 870 | ] 871 | 872 | [[package]] 873 | name = "winapi-x86_64-pc-windows-gnu" 874 | version = "0.4.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 877 | 878 | [[package]] 879 | name = "windows-sys" 880 | version = "0.42.0" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 883 | dependencies = [ 884 | "windows_aarch64_gnullvm", 885 | "windows_aarch64_msvc", 886 | "windows_i686_gnu", 887 | "windows_i686_msvc", 888 | "windows_x86_64_gnu", 889 | "windows_x86_64_gnullvm", 890 | "windows_x86_64_msvc", 891 | ] 892 | 893 | [[package]] 894 | name = "windows-sys" 895 | version = "0.45.0" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 898 | dependencies = [ 899 | "windows-targets", 900 | ] 901 | 902 | [[package]] 903 | name = "windows-targets" 904 | version = "0.42.1" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 907 | dependencies = [ 908 | "windows_aarch64_gnullvm", 909 | "windows_aarch64_msvc", 910 | "windows_i686_gnu", 911 | "windows_i686_msvc", 912 | "windows_x86_64_gnu", 913 | "windows_x86_64_gnullvm", 914 | "windows_x86_64_msvc", 915 | ] 916 | 917 | [[package]] 918 | name = "windows_aarch64_gnullvm" 919 | version = "0.42.1" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 922 | 923 | [[package]] 924 | name = "windows_aarch64_msvc" 925 | version = "0.42.1" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 928 | 929 | [[package]] 930 | name = "windows_i686_gnu" 931 | version = "0.42.1" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 934 | 935 | [[package]] 936 | name = "windows_i686_msvc" 937 | version = "0.42.1" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 940 | 941 | [[package]] 942 | name = "windows_x86_64_gnu" 943 | version = "0.42.1" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 946 | 947 | [[package]] 948 | name = "windows_x86_64_gnullvm" 949 | version = "0.42.1" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 952 | 953 | [[package]] 954 | name = "windows_x86_64_msvc" 955 | version = "0.42.1" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 958 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-stock" 3 | version = "1.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | # Log 和 SimpleLogger在 TUI 应用里看不到 10 | # log = "0.4" 11 | # simple_logger = "1.16" 12 | 13 | # 用 Rust 构建终端用户界面的框架 14 | # crossterm 支持 windows, 但性能比 termion 稍差,旧版本鼠标支持有问题 15 | # 使用 tui-rs 和 crossterm 启动一个控制台的终端界面 16 | tui = { version = "0.19", default-features = false, features = ['crossterm', 'serde'] } 17 | crossterm = { version = "0.25", features = [ "serde" ] } 18 | # 系列化 library 19 | serde = {version = "1.0", features = ["derive"] } 20 | serde_json = "1.0" 21 | # time library 22 | chrono = "0.4" 23 | 24 | # 解决tui里中文宽度的计算 25 | unicode-width = "0.1" 26 | 27 | # reqwest太大了3M, ureq也有2M, http_req只有300k 28 | http_req = "0.9" 29 | 30 | # 查询跨平台的通用目录位置 31 | dirs-next = "2.0" 32 | 33 | # 懒加载 crate 34 | # lazy_static = "1.4.0" 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-stock 2 | 3 | Terminal Stock Query 4 | 5 | A simple terminal tool for stock query written in Rust 🦀 6 | 7 | 使用 Rust 开发的股价查询终端应用 8 | 9 | ![](images/rust-stock.gif) 10 | 11 | 12 | ## 引用库介绍 13 | 14 | ``` 15 | tui-rs 是一款超好用的跨平台命令行界面库 16 | 17 | 使用 tui.rs 提供的以下模块进行 UI 编写(所有 UI 元素都实现了 Widget 或 StatefuWidget Trait): 18 | 19 | bakend 用于生成管理命令行的后端 20 | layout 用于管理 UI 组件的布局 21 | style 用于为 UI 添加样式 22 | symbols 描述绘制散点图时所用点的样式 23 | text 用于描述带样式的文本 24 | widgets 包含预定义的 UI 组件 25 | 26 | 项目地址:https://github.com/fdehau/tui-rs 27 | 官方文档:https://docs.rs/tui/latest/tui/index.html 28 | 29 | tui介绍:https://www.51cto.com/article/703696.html 30 | 实时股票数据: https://github.com/tarkah/tickrs 31 | 文件传输工具:https://github.com/veeso/termscp 32 | 网络监控工具:https://github.com/imsnif/bandwhich 33 | 34 | ``` 35 | 36 | # 编译/运行 37 | 38 | ``` 39 | ➜ git https://github.com/huangbqsky/rust-stock 40 | ➜ cd rust-stock 41 | ➜ cargo build 42 | ➜ cargo run 43 | ``` 44 | 45 | # 参考 46 | - https://github.com/x1y9/rust-stock 47 | 48 | - https://github.com/chessbr/rust-exchange 49 | -------------------------------------------------------------------------------- /images/rust-stock.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huangbqsky/rust-stock/53cf0ffa25c4ff503a964c6119d207f7176ff204/images/rust-stock.gif -------------------------------------------------------------------------------- /src/aio.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{channel, Sender}; 2 | 3 | // 异步执行器 4 | #[derive(Clone)] 5 | pub struct Executor { 6 | task_sender: Sender, 7 | } 8 | pub enum Task { 9 | Println(String), 10 | Exit, 11 | } 12 | 13 | impl Executor { 14 | pub fn new() -> Self { 15 | let (sender, receiver) = channel(); 16 | std::thread::spawn(move || loop { 17 | match receiver.recv() { 18 | Ok(task) => match task { 19 | Task::Println(string) => println!("{}", string), 20 | Task::Exit => return, 21 | }, 22 | Err(_) => { 23 | return; 24 | } 25 | } 26 | }); 27 | Executor { 28 | task_sender: sender, 29 | } 30 | } 31 | 32 | pub fn println(&self, string: String) { 33 | self.task_sender.send(Task::Println(string)).unwrap() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{Event, KeyCode, MouseEventKind}; 2 | 3 | use crate::stock::{App, AppState, Stock}; 4 | 5 | // 处理键盘、鼠标事件 6 | pub fn on_events(event: Event, app: &mut App) { 7 | let total = app.stocks.lock().unwrap().len(); 8 | let sel = app.stocks_state.selected().unwrap_or(0); 9 | let selsome = app.stocks_state.selected().is_some() && sel < total; 10 | 11 | match app.state { 12 | AppState::Normal => { 13 | if let Event::Key(key) = event { 14 | let code = key.code; 15 | if code == KeyCode::Char('q') { 16 | app.should_exit = true; 17 | } else if code == KeyCode::Char('r') { 18 | app.refresh_stocks(); 19 | } else if code == KeyCode::Char('n') { 20 | // 新建stock 21 | app.state = AppState::Adding; 22 | app.input = String::new(); 23 | } else if code == KeyCode::Char('d') && selsome { 24 | // 删除当前选中的stock 25 | app.stocks.lock().unwrap().remove(sel); 26 | app.save_stocks().unwrap(); 27 | app.stocks_state.select(None); 28 | } else if code == KeyCode::Char('u') && selsome && sel > 0 { 29 | // 将选中stock往上移动一位 30 | app.stocks.lock().unwrap().swap(sel, sel - 1); 31 | app.save_stocks().unwrap(); 32 | app.stocks_state.select(Some(sel - 1)); 33 | } else if code == KeyCode::Char('j') && selsome && sel < total - 1 { 34 | // 将选中stock往下移动一位 35 | app.stocks.lock().unwrap().swap(sel, sel + 1); 36 | app.save_stocks().unwrap(); 37 | app.stocks_state.select(Some(sel + 1)); 38 | } else if code == KeyCode::Up && total > 0 { 39 | // 注意这里如果不加判断直接用sel - 1, 在sel为0时会导致异常 40 | app.stocks_state 41 | .select(Some(if sel > 0 { sel - 1 } else { 0 })); 42 | } else if code == KeyCode::Down && total > 0 { 43 | app.stocks_state 44 | .select(Some(if sel < total - 1 { sel + 1 } else { sel })); 45 | } 46 | } else if let Event::Mouse(mouse) = event { 47 | if let MouseEventKind::Up(_button) = mouse.kind { 48 | let row = mouse.row as usize; 49 | // list是从第三行开始,所以要减去2, 这里本来还应该考虑list的滚动, 50 | // 但是app.stocks_state的滚动位置字段是private的,取不到。 51 | if row >= 2 && row < total + 2 { 52 | app.stocks_state.select(Some(row - 2)); 53 | } 54 | } 55 | } 56 | } 57 | 58 | AppState::Adding => match event { 59 | Event::Key(key) => match key.code { 60 | KeyCode::Enter => { 61 | app.state = AppState::Normal; 62 | if app.input.len() > 0 { 63 | app.stocks.lock().unwrap().push(Stock::new(&app.input)); 64 | app.refresh_stocks(); 65 | app.save_stocks().unwrap(); 66 | } 67 | } 68 | KeyCode::Esc => { 69 | app.state = AppState::Normal; 70 | } 71 | KeyCode::Char(c) => { 72 | app.input.push(c); 73 | } 74 | KeyCode::Backspace => { 75 | app.input.pop(); 76 | } 77 | _ => {} 78 | }, 79 | _ => {} 80 | }, 81 | } 82 | } 83 | 84 | // 处理定时事件 85 | pub fn on_tick(app: &mut App) { 86 | app.tick_count += 1; 87 | if app.tick_count % 60 == 0 { 88 | if let AppState::Normal = app.state { 89 | app.refresh_stocks(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod stock; // 股票结构体 2 | pub mod widget; // UI框架 3 | pub mod events; // 键盘、鼠标事件 4 | pub mod aio; // 异步任务 5 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | time::{Duration, Instant}, 4 | }; 5 | 6 | use rust_stock::stock::{App, AppState, CrossTerminal, DynResult, TerminalFrame}; 7 | use rust_stock::{events, widget}; 8 | 9 | use tui::{backend::CrosstermBackend, widgets, Terminal}; 10 | use unicode_width::UnicodeWidthStr; 11 | 12 | fn main() -> DynResult { 13 | let mut app = App::new(); 14 | let mut terminal = init_terminal()?; 15 | main_loop(&mut terminal, &mut app)?; 16 | close_terminal(terminal)?; 17 | 18 | Ok(()) 19 | } 20 | 21 | // 初始化 app 终端 22 | fn init_terminal() -> Result> { 23 | let mut stdout = std::io::stdout(); 24 | crossterm::terminal::enable_raw_mode()?; 25 | // 必须先执行EnableMouseCapture后面才能支持鼠标事件 26 | crossterm::execute!( 27 | stdout, 28 | crossterm::terminal::EnterAlternateScreen, 29 | crossterm::event::EnableMouseCapture 30 | )?; 31 | let backend = CrosstermBackend::new(stdout); 32 | let mut terminal = Terminal::new(backend)?; 33 | terminal.clear()?; 34 | Ok(terminal) 35 | } 36 | 37 | // 关闭 app 终端 38 | fn close_terminal(mut terminal: CrossTerminal) -> DynResult { 39 | crossterm::terminal::disable_raw_mode()?; 40 | crossterm::execute!( 41 | terminal.backend_mut(), 42 | crossterm::event::DisableMouseCapture, 43 | crossterm::terminal::LeaveAlternateScreen 44 | )?; 45 | Ok(()) 46 | } 47 | 48 | // 主事件循环 49 | fn main_loop(terminal: &mut CrossTerminal, app: &mut App) -> DynResult { 50 | let mut last_tick = Instant::now(); 51 | while !app.should_exit { 52 | terminal.draw(|f| { 53 | on_draw(f, app); 54 | })?; 55 | 56 | if crossterm::event::poll( 57 | Duration::from_secs(1) 58 | .checked_sub(last_tick.elapsed()) 59 | .unwrap_or_default(), 60 | )? { 61 | events::on_events(crossterm::event::read()?, app); 62 | } else { 63 | events::on_tick(app); 64 | last_tick = Instant::now(); 65 | } 66 | } 67 | 68 | Ok(()) 69 | } 70 | 71 | // 绘制 72 | fn on_draw(frame: &mut TerminalFrame, app: &mut App) { 73 | let chunks = widget::main_chunks(frame.size()); 74 | 75 | // list的render需要调render_stateful_widget,否则滚动状态不对, 76 | // 这里第一个参数不能是app,否则会和后面的mut stock_state冲突 77 | frame.render_stateful_widget( 78 | widget::stock_list(&app.stocks.lock().unwrap()), 79 | chunks[1], 80 | &mut app.stocks_state, 81 | ); 82 | // 因为render stock_list时会修改滚动状态,后面如果要用到这个值,就需要先做list的render 83 | frame.render_widget(widget::title_bar(app, frame.size()), chunks[0]); 84 | frame.render_widget(widget::stock_detail(app), chunks[2]); 85 | frame.render_widget(widget::status_bar(app), chunks[3]); 86 | 87 | if let AppState::Adding = app.state { 88 | // popup需要先clear一下,否则下面的背景色会透上来 89 | frame.render_widget(widgets::Clear, chunks[4]); 90 | frame.render_widget(widget::stock_input(app), chunks[4]); 91 | 92 | // 显示光标, width()接口依赖一个外部包,可以正确处理中文宽度 93 | frame.set_cursor(chunks[4].x + app.input.width() as u16 + 1, chunks[4].y + 1); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/stock.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{ 4 | collections::HashMap, 5 | fs, 6 | io::Stdout, 7 | sync::{Arc, Mutex}, 8 | thread, 9 | }; 10 | 11 | use chrono::{DateTime, Local}; // 时间 library 12 | use http_req::request; // 网络 library 13 | use serde::{Deserialize, Serialize}; // 序列化 library 14 | use serde_json::{json, Map, Value}; // 序列化 library 15 | /** 16 | tui:一款超好用的跨平台命令行界面库 17 | 使用 tui.rs 提供的以下模块进行 UI 编写(所有 UI 元素都实现了 Widget 或 StatefuWidget Trait): 18 | 19 | bakend 用于生成管理命令行的后端 20 | layout 用于管理 UI 组件的布局 21 | style 用于为 UI 添加样式 22 | symbols 描述绘制散点图时所用点的样式 23 | text 用于描述带样式的文本 24 | widgets 包含预定义的 UI 组件 25 | 26 | 项目地址:https://github.com/fdehau/tui-rs 27 | 官方文档:https://docs.rs/tui/latest/tui/index.html 28 | 29 | tui介绍:https://www.51cto.com/article/703696.html 30 | 实时股票数据: https://github.com/tarkah/tickrs 31 | 文件传输工具:https://github.com/veeso/termscp 32 | 网络监控工具:https://github.com/imsnif/bandwhich 33 | 34 | */ 35 | use tui::{backend::CrosstermBackend, widgets::ListState}; // UI library 36 | 37 | pub type DynResult = Result<(), Box>; // Ruesut的类型别名, Err是Trait Object 属于动态类型 38 | pub type CrossTerminal = tui::Terminal>; // 终端后端的类型别名, 39 | pub type TerminalFrame<'a> = tui::Frame<'a, CrosstermBackend>; // 终端Frame的类型别名, 40 | 41 | pub const DB_PATH: &str = ".stocks.json"; // 模拟数据库 42 | 43 | // 股票信息结构体 44 | #[derive(Serialize, Deserialize, Clone, Debug)] 45 | pub struct Stock { 46 | pub title: String, // 股票名称 47 | pub code: String, // 股票代码 48 | pub price: f64, // 股票价格 49 | pub percent: f64, // 股票涨跌 50 | pub open: f64, // 今开 51 | pub yestclose: f64, // 昨收 52 | pub high: f64, // 最高 53 | pub low: f64, // 最低 54 | } 55 | 56 | impl Stock { 57 | pub fn new(code: &String) -> Self { 58 | Self { 59 | code: code.clone(), 60 | title: code.clone(), 61 | price: 0.0, 62 | percent: 0.0, 63 | open: 0.0, 64 | yestclose: 0.0, 65 | high: 0.0, 66 | low: 0.0, 67 | } 68 | } 69 | } 70 | 71 | pub enum AppState { 72 | Normal, 73 | Adding, 74 | } 75 | 76 | pub struct App { 77 | pub should_exit: bool, 78 | pub state: AppState, 79 | pub error: Arc>, 80 | pub input: String, 81 | pub stocks: Arc>>, 82 | // TUI的List控件需要这个state记录当前选中和滚动位置两个状态 83 | pub stocks_state: ListState, 84 | pub last_refresh: Arc>>, 85 | pub tick_count: u128, 86 | } 87 | 88 | impl App { 89 | pub fn new() -> Self { 90 | let mut app = Self { 91 | should_exit: false, 92 | state: AppState::Normal, 93 | input: String::new(), 94 | error: Arc::new(Mutex::new(String::new())), 95 | stocks: Arc::new(Mutex::new([].to_vec())), 96 | // ListState:default为未选择,因为可能 stocks 为空,所以不能自动选第一个 97 | stocks_state: ListState::default(), 98 | last_refresh: Arc::new(Mutex::new(Local::now())), 99 | tick_count: 0, 100 | }; 101 | app.load_stocks().unwrap_or_default(); 102 | app.refresh_stocks(); 103 | app 104 | } 105 | pub fn save_stocks(&self) -> DynResult { 106 | let db = dirs_next::home_dir().unwrap().join(DB_PATH); 107 | // 每个 stock 单独存一个对象,是考虑将来的扩展性 108 | let stocks = self.stocks.lock().unwrap(); 109 | let lists: Vec<_> = stocks 110 | .iter() 111 | .map(|s| HashMap::from([("code", &s.code)])) 112 | .collect(); 113 | fs::write( 114 | &db, 115 | serde_json::to_string(&HashMap::from([("stocks", lists)]))?, 116 | )?; 117 | Ok(()) 118 | } 119 | 120 | pub fn load_stocks(&mut self) -> DynResult { 121 | // 用unwrap_or_default屏蔽文件不存在时的异常 122 | let content = 123 | fs::read_to_string(dirs_next::home_dir().unwrap().join(DB_PATH)).unwrap_or_default(); 124 | // 如果直接转换stocks,必须所有key都对上, 兼容性不好 125 | // self.stocks = serde_json::from_str(&content).unwrap_or_default(); 126 | 127 | // 先读成Map再转换,可以增加兼容性, 128 | let json: Map = serde_json::from_str(&content).unwrap_or_default(); 129 | let mut data = self.stocks.lock().unwrap(); 130 | data.clear(); 131 | data.append( 132 | &mut json 133 | .get("stocks") 134 | .unwrap_or(&json!([])) 135 | .as_array() 136 | .unwrap() 137 | .iter() 138 | .map(|s| { 139 | Stock::new( 140 | &s.as_object() 141 | .unwrap() 142 | .get("code") 143 | .unwrap() 144 | .as_str() 145 | .unwrap() 146 | .to_string(), 147 | ) 148 | }) 149 | .collect(), 150 | ); 151 | 152 | Ok(()) 153 | } 154 | 155 | pub fn refresh_stocks(&mut self) { 156 | let stock_clone = self.stocks.clone(); 157 | let err_clone = self.error.clone(); 158 | let last_refresh_clone = self.last_refresh.clone(); 159 | let codes = self.get_codes(); 160 | if codes.len() > 0 { 161 | thread::spawn(move || { 162 | let mut writer = Vec::new(); 163 | let ret = request::get( 164 | format!("{}{}", "http://api.money.126.net/data/feed/", codes), 165 | &mut writer, 166 | ); 167 | let mut locked_err = err_clone.lock().unwrap(); 168 | if let Err(err) = ret { 169 | *locked_err = format!("{:?}", err); 170 | } else { 171 | let content = String::from_utf8_lossy(&writer); 172 | if content.starts_with("_ntes_quote_callback") { 173 | let mut stocks = stock_clone.lock().unwrap(); 174 | // 网易的返回包了一个js call,用skip,take,collect实现一个substring剥掉它 175 | let json: Map = serde_json::from_str( 176 | &content 177 | .chars() 178 | .skip(21) 179 | .take(content.len() - 23) 180 | .collect::(), 181 | ) 182 | .unwrap(); 183 | for stock in stocks.iter_mut() { 184 | // 如果code不对,返回的json里不包括这个对象, 用unwrap_or生成一个空对象,防止异常 185 | let obj = json 186 | .get(&stock.code) 187 | .unwrap_or(&json!({})) 188 | .as_object() 189 | .unwrap() 190 | .to_owned(); 191 | stock.title = obj 192 | .get("name") 193 | .unwrap_or(&json!(stock.code.clone())) 194 | .as_str() 195 | .unwrap() 196 | .to_owned(); 197 | stock.price = obj.get("price").unwrap_or(&json!(0.0)).as_f64().unwrap(); 198 | stock.percent = 199 | obj.get("percent").unwrap_or(&json!(0.0)).as_f64().unwrap(); 200 | stock.open = obj.get("open").unwrap_or(&json!(0.0)).as_f64().unwrap(); 201 | stock.yestclose = obj 202 | .get("yestclose") 203 | .unwrap_or(&json!(0.0)) 204 | .as_f64() 205 | .unwrap(); 206 | stock.high = obj.get("high").unwrap_or(&json!(0.0)).as_f64().unwrap(); 207 | stock.low = obj.get("low").unwrap_or(&json!(0.0)).as_f64().unwrap(); 208 | 209 | // if json.contains_key(&stock.code) { 210 | // let mut writer2 = Vec::new(); 211 | // request::get(format!("http://img1.money.126.net/data/hs/time/today/{}.json",stock.code), &mut writer2)?; 212 | // println!("{:?}", format!("http://img1.money.126.net/data/hs/time/today/{}.json",stock.code)); 213 | // let json2: Map = serde_json::from_str(&String::from_utf8_lossy(&writer2).to_string())?; 214 | // stock.slice = json2.get("data").unwrap().as_array().unwrap() 215 | // .iter().map(|item| item.as_array().unwrap().get(2).unwrap().as_f64().unwrap()) 216 | // .collect(); 217 | // } 218 | } 219 | let mut last_refresh = last_refresh_clone.lock().unwrap(); 220 | *last_refresh = Local::now(); 221 | *locked_err = String::new(); 222 | } else { 223 | *locked_err = String::from("服务器返回错误"); 224 | } 225 | } 226 | }); 227 | } 228 | } 229 | 230 | // 返回股票代码串 231 | pub fn get_codes(&self) -> String { 232 | let codes: Vec = self 233 | .stocks 234 | .lock() 235 | .unwrap() 236 | .iter() 237 | .map(|stock| stock.code.clone()) 238 | .collect(); 239 | codes.join(",") 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/widget.rs: -------------------------------------------------------------------------------- 1 | use tui::{ 2 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 3 | style::{Color, Modifier, Style}, 4 | text::{Span, Spans}, 5 | widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, 6 | }; 7 | 8 | use crate::stock::{App, AppState, Stock}; 9 | use unicode_width::UnicodeWidthStr; 10 | 11 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 12 | 13 | // 计算所有的屏幕窗口区域,供后续 render 使用 14 | pub fn main_chunks(area: Rect) -> Vec { 15 | let parent = Layout::default() 16 | .direction(Direction::Vertical) 17 | .constraints( 18 | [ 19 | Constraint::Length(1), 20 | Constraint::Min(1), 21 | Constraint::Length(1), 22 | ] 23 | .as_ref(), 24 | ) 25 | .split(area); 26 | 27 | let center = Layout::default() 28 | .direction(Direction::Horizontal) 29 | .margin(0) 30 | .constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref()) 31 | .split(parent[1]); 32 | 33 | // 计算新建stock时的弹框位置 34 | let popup = Layout::default() 35 | .direction(Direction::Vertical) 36 | .constraints( 37 | [ 38 | Constraint::Percentage(40), 39 | Constraint::Length(3), 40 | Constraint::Percentage(40), 41 | ] 42 | .as_ref(), 43 | ) 44 | .split(area); 45 | 46 | let popline = Layout::default() 47 | .direction(Direction::Horizontal) 48 | .margin(0) 49 | .constraints( 50 | [ 51 | Constraint::Percentage(10), 52 | Constraint::Percentage(80), 53 | Constraint::Percentage(10), 54 | ] 55 | .as_ref(), 56 | ) 57 | .split(popup[1]); 58 | 59 | vec![parent[0], center[0], center[1], parent[2], popline[1]] 60 | } 61 | 62 | // 股票列表 63 | pub fn stock_list(stocks: &Vec) -> List { 64 | let items: Vec<_> = stocks 65 | .iter() 66 | .map(|stock| { 67 | ListItem::new(Spans::from(vec![ 68 | Span::styled( 69 | format!("{:+.2}% ", stock.percent * 100.0), 70 | Style::default().fg(if stock.percent < 0.0 { 71 | Color::Green 72 | } else { 73 | Color::Red 74 | }), 75 | ), 76 | Span::styled(stock.title.clone(), Style::default()), 77 | ])) 78 | }) 79 | .collect(); 80 | 81 | List::new(items) 82 | .block( 83 | Block::default() 84 | .borders(Borders::ALL) 85 | .style(Style::default().fg(Color::White)) 86 | .title("列表") 87 | .border_type(BorderType::Plain), 88 | ) 89 | .highlight_style( 90 | Style::default() 91 | .bg(Color::Yellow) 92 | .fg(Color::Black) 93 | .add_modifier(Modifier::BOLD), 94 | ) 95 | } 96 | 97 | // 股票详情 98 | pub fn stock_detail(app: &App) -> Paragraph { 99 | let mut info = String::new(); 100 | let sel = app.stocks_state.selected().unwrap_or(0); 101 | // 这里要防止sel超出列表范围 102 | let stocks = app.stocks.lock().unwrap(); 103 | if app.stocks_state.selected().is_some() && sel < stocks.len() { 104 | let stock = stocks.get(sel).unwrap(); 105 | info = format!( 106 | "代码:{}\n涨跌:{:+.2}%\n当前:{}\n今开:{}\n昨收:{}\n最高:{}\n最低:{}", 107 | stock.code, 108 | stock.percent * 100.0, 109 | stock.price, 110 | stock.open, 111 | stock.yestclose, 112 | stock.high, 113 | stock.low 114 | ); 115 | } 116 | 117 | Paragraph::new(info) 118 | .alignment(Alignment::Center) 119 | .style(Style::default()) 120 | .block( 121 | Block::default() 122 | .title("详情") 123 | .borders(Borders::ALL) 124 | .border_type(BorderType::Plain), 125 | ) 126 | } 127 | 128 | // 插入一个股票 129 | pub fn stock_input(app: &App) -> Paragraph { 130 | Paragraph::new(app.input.as_ref()) 131 | .style(Style::default().fg(Color::Yellow)) 132 | .block(Block::default().borders(Borders::ALL).title("输入证券代码")) 133 | } 134 | 135 | // title bar 信息 : 【左上】Stock v0.1.0 ---------- 【右上】最后更新 16:15:15 136 | pub fn title_bar(app: &App, rect: Rect) -> Paragraph { 137 | let left = format!("Stock v{}", VERSION); 138 | let error = app.error.lock().unwrap(); 139 | let right = if error.is_empty() { 140 | app.last_refresh 141 | .lock() 142 | .unwrap() 143 | .format("最后更新 %H:%M:%S") 144 | .to_string() 145 | } else { 146 | error.clone() 147 | }; 148 | Paragraph::new(Spans::from(vec![ 149 | Span::raw(left.clone()), 150 | // 使用checked_sub防止溢出 151 | Span::raw( 152 | " ".repeat( 153 | (rect.width as usize) 154 | .checked_sub(right.width() + left.width()) 155 | .unwrap_or(0), 156 | ), 157 | ), 158 | Span::styled( 159 | right, 160 | Style::default().fg(if error.is_empty() { 161 | Color::White 162 | } else { 163 | Color::Red 164 | }), 165 | ), 166 | ])) 167 | .alignment(Alignment::Left) 168 | } 169 | 170 | // 底部操作拦: 171 | pub fn status_bar(app: &mut App) -> Paragraph { 172 | Paragraph::new( 173 | match app.state { 174 | AppState::Normal => "退出[Q] | 新建[N] | 删除[D] | 刷新[R] | 上移[U] | 下移[J]", 175 | AppState::Adding => "确认[Enter] | 取消[ESC] | 上交所代码前需要加0,深市加1", 176 | } 177 | .to_string(), 178 | ) 179 | .alignment(Alignment::Left) 180 | } 181 | --------------------------------------------------------------------------------