├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── build.bat ├── images └── screen.gif └── src ├── aio.rs ├── events.rs ├── lib.rs ├── main.rs └── widget.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.exe 3 | -------------------------------------------------------------------------------- /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 = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "1.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 16 | 17 | [[package]] 18 | name = "cassowary" 19 | version = "0.3.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 22 | 23 | [[package]] 24 | name = "cc" 25 | version = "1.0.72" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" 28 | 29 | [[package]] 30 | name = "cfg-if" 31 | version = "1.0.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 34 | 35 | [[package]] 36 | name = "chrono" 37 | version = "0.4.19" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 40 | dependencies = [ 41 | "libc", 42 | "num-integer", 43 | "num-traits", 44 | "time", 45 | "winapi", 46 | ] 47 | 48 | [[package]] 49 | name = "core-foundation" 50 | version = "0.9.3" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 53 | dependencies = [ 54 | "core-foundation-sys", 55 | "libc", 56 | ] 57 | 58 | [[package]] 59 | name = "core-foundation-sys" 60 | version = "0.8.3" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 63 | 64 | [[package]] 65 | name = "crossterm" 66 | version = "0.18.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "4e86d73f2a0b407b5768d10a8c720cf5d2df49a9efc10ca09176d201ead4b7fb" 69 | dependencies = [ 70 | "bitflags", 71 | "crossterm_winapi 0.6.2", 72 | "lazy_static", 73 | "libc", 74 | "mio", 75 | "parking_lot 0.11.2", 76 | "signal-hook 0.1.17", 77 | "winapi", 78 | ] 79 | 80 | [[package]] 81 | name = "crossterm" 82 | version = "0.23.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "77b75a27dc8d220f1f8521ea69cd55a34d720a200ebb3a624d9aa19193d3b432" 85 | dependencies = [ 86 | "bitflags", 87 | "crossterm_winapi 0.9.0", 88 | "libc", 89 | "mio", 90 | "parking_lot 0.12.0", 91 | "serde", 92 | "signal-hook 0.3.13", 93 | "signal-hook-mio", 94 | "winapi", 95 | ] 96 | 97 | [[package]] 98 | name = "crossterm_winapi" 99 | version = "0.6.2" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "c2265c3f8e080075d9b6417aa72293fc71662f34b4af2612d8d1b074d29510db" 102 | dependencies = [ 103 | "winapi", 104 | ] 105 | 106 | [[package]] 107 | name = "crossterm_winapi" 108 | version = "0.9.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" 111 | dependencies = [ 112 | "winapi", 113 | ] 114 | 115 | [[package]] 116 | name = "dirs-next" 117 | version = "2.0.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 120 | dependencies = [ 121 | "cfg-if", 122 | "dirs-sys-next", 123 | ] 124 | 125 | [[package]] 126 | name = "dirs-sys-next" 127 | version = "0.1.2" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 130 | dependencies = [ 131 | "libc", 132 | "redox_users", 133 | "winapi", 134 | ] 135 | 136 | [[package]] 137 | name = "fastrand" 138 | version = "1.7.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 141 | dependencies = [ 142 | "instant", 143 | ] 144 | 145 | [[package]] 146 | name = "foreign-types" 147 | version = "0.3.2" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 150 | dependencies = [ 151 | "foreign-types-shared", 152 | ] 153 | 154 | [[package]] 155 | name = "foreign-types-shared" 156 | version = "0.1.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 159 | 160 | [[package]] 161 | name = "getrandom" 162 | version = "0.2.4" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" 165 | dependencies = [ 166 | "cfg-if", 167 | "libc", 168 | "wasi", 169 | ] 170 | 171 | [[package]] 172 | name = "http_req" 173 | version = "0.8.1" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "9e6cd45a270dff33553602fd84beb02c89460ee32db638715f10d9060389fd6a" 176 | dependencies = [ 177 | "native-tls", 178 | "unicase", 179 | ] 180 | 181 | [[package]] 182 | name = "instant" 183 | version = "0.1.12" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 186 | dependencies = [ 187 | "cfg-if", 188 | ] 189 | 190 | [[package]] 191 | name = "itoa" 192 | version = "1.0.1" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 195 | 196 | [[package]] 197 | name = "lazy_static" 198 | version = "1.4.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 201 | 202 | [[package]] 203 | name = "libc" 204 | version = "0.2.117" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" 207 | 208 | [[package]] 209 | name = "lock_api" 210 | version = "0.4.6" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" 213 | dependencies = [ 214 | "scopeguard", 215 | ] 216 | 217 | [[package]] 218 | name = "log" 219 | version = "0.4.14" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 222 | dependencies = [ 223 | "cfg-if", 224 | ] 225 | 226 | [[package]] 227 | name = "mio" 228 | version = "0.7.14" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" 231 | dependencies = [ 232 | "libc", 233 | "log", 234 | "miow", 235 | "ntapi", 236 | "winapi", 237 | ] 238 | 239 | [[package]] 240 | name = "miow" 241 | version = "0.3.7" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 244 | dependencies = [ 245 | "winapi", 246 | ] 247 | 248 | [[package]] 249 | name = "native-tls" 250 | version = "0.2.8" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" 253 | dependencies = [ 254 | "lazy_static", 255 | "libc", 256 | "log", 257 | "openssl", 258 | "openssl-probe", 259 | "openssl-sys", 260 | "schannel", 261 | "security-framework", 262 | "security-framework-sys", 263 | "tempfile", 264 | ] 265 | 266 | [[package]] 267 | name = "ntapi" 268 | version = "0.3.7" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" 271 | dependencies = [ 272 | "winapi", 273 | ] 274 | 275 | [[package]] 276 | name = "num-integer" 277 | version = "0.1.44" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 280 | dependencies = [ 281 | "autocfg", 282 | "num-traits", 283 | ] 284 | 285 | [[package]] 286 | name = "num-traits" 287 | version = "0.2.14" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 290 | dependencies = [ 291 | "autocfg", 292 | ] 293 | 294 | [[package]] 295 | name = "once_cell" 296 | version = "1.9.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" 299 | 300 | [[package]] 301 | name = "openssl" 302 | version = "0.10.38" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" 305 | dependencies = [ 306 | "bitflags", 307 | "cfg-if", 308 | "foreign-types", 309 | "libc", 310 | "once_cell", 311 | "openssl-sys", 312 | ] 313 | 314 | [[package]] 315 | name = "openssl-probe" 316 | version = "0.1.5" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 319 | 320 | [[package]] 321 | name = "openssl-sys" 322 | version = "0.9.72" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" 325 | dependencies = [ 326 | "autocfg", 327 | "cc", 328 | "libc", 329 | "pkg-config", 330 | "vcpkg", 331 | ] 332 | 333 | [[package]] 334 | name = "parking_lot" 335 | version = "0.11.2" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 338 | dependencies = [ 339 | "instant", 340 | "lock_api", 341 | "parking_lot_core 0.8.5", 342 | ] 343 | 344 | [[package]] 345 | name = "parking_lot" 346 | version = "0.12.0" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" 349 | dependencies = [ 350 | "lock_api", 351 | "parking_lot_core 0.9.1", 352 | ] 353 | 354 | [[package]] 355 | name = "parking_lot_core" 356 | version = "0.8.5" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 359 | dependencies = [ 360 | "cfg-if", 361 | "instant", 362 | "libc", 363 | "redox_syscall", 364 | "smallvec", 365 | "winapi", 366 | ] 367 | 368 | [[package]] 369 | name = "parking_lot_core" 370 | version = "0.9.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" 373 | dependencies = [ 374 | "cfg-if", 375 | "libc", 376 | "redox_syscall", 377 | "smallvec", 378 | "windows-sys", 379 | ] 380 | 381 | [[package]] 382 | name = "pkg-config" 383 | version = "0.3.24" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" 386 | 387 | [[package]] 388 | name = "proc-macro2" 389 | version = "1.0.36" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 392 | dependencies = [ 393 | "unicode-xid", 394 | ] 395 | 396 | [[package]] 397 | name = "quote" 398 | version = "1.0.15" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" 401 | dependencies = [ 402 | "proc-macro2", 403 | ] 404 | 405 | [[package]] 406 | name = "redox_syscall" 407 | version = "0.2.10" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 410 | dependencies = [ 411 | "bitflags", 412 | ] 413 | 414 | [[package]] 415 | name = "redox_users" 416 | version = "0.4.0" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 419 | dependencies = [ 420 | "getrandom", 421 | "redox_syscall", 422 | ] 423 | 424 | [[package]] 425 | name = "remove_dir_all" 426 | version = "0.5.3" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 429 | dependencies = [ 430 | "winapi", 431 | ] 432 | 433 | [[package]] 434 | name = "ryu" 435 | version = "1.0.9" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 438 | 439 | [[package]] 440 | name = "schannel" 441 | version = "0.1.19" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" 444 | dependencies = [ 445 | "lazy_static", 446 | "winapi", 447 | ] 448 | 449 | [[package]] 450 | name = "scopeguard" 451 | version = "1.1.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 454 | 455 | [[package]] 456 | name = "security-framework" 457 | version = "2.6.1" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" 460 | dependencies = [ 461 | "bitflags", 462 | "core-foundation", 463 | "core-foundation-sys", 464 | "libc", 465 | "security-framework-sys", 466 | ] 467 | 468 | [[package]] 469 | name = "security-framework-sys" 470 | version = "2.6.1" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 473 | dependencies = [ 474 | "core-foundation-sys", 475 | "libc", 476 | ] 477 | 478 | [[package]] 479 | name = "serde" 480 | version = "1.0.136" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" 483 | dependencies = [ 484 | "serde_derive", 485 | ] 486 | 487 | [[package]] 488 | name = "serde_derive" 489 | version = "1.0.136" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" 492 | dependencies = [ 493 | "proc-macro2", 494 | "quote", 495 | "syn", 496 | ] 497 | 498 | [[package]] 499 | name = "serde_json" 500 | version = "1.0.78" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" 503 | dependencies = [ 504 | "itoa", 505 | "ryu", 506 | "serde", 507 | ] 508 | 509 | [[package]] 510 | name = "signal-hook" 511 | version = "0.1.17" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" 514 | dependencies = [ 515 | "libc", 516 | "mio", 517 | "signal-hook-registry", 518 | ] 519 | 520 | [[package]] 521 | name = "signal-hook" 522 | version = "0.3.13" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" 525 | dependencies = [ 526 | "libc", 527 | "signal-hook-registry", 528 | ] 529 | 530 | [[package]] 531 | name = "signal-hook-mio" 532 | version = "0.2.1" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" 535 | dependencies = [ 536 | "libc", 537 | "mio", 538 | "signal-hook 0.3.13", 539 | ] 540 | 541 | [[package]] 542 | name = "signal-hook-registry" 543 | version = "1.4.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 546 | dependencies = [ 547 | "libc", 548 | ] 549 | 550 | [[package]] 551 | name = "smallvec" 552 | version = "1.8.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 555 | 556 | [[package]] 557 | name = "stock" 558 | version = "1.4.0" 559 | dependencies = [ 560 | "chrono", 561 | "crossterm 0.23.0", 562 | "dirs-next", 563 | "http_req", 564 | "serde", 565 | "serde_json", 566 | "tui", 567 | "unicode-width", 568 | ] 569 | 570 | [[package]] 571 | name = "syn" 572 | version = "1.0.86" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" 575 | dependencies = [ 576 | "proc-macro2", 577 | "quote", 578 | "unicode-xid", 579 | ] 580 | 581 | [[package]] 582 | name = "tempfile" 583 | version = "3.3.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 586 | dependencies = [ 587 | "cfg-if", 588 | "fastrand", 589 | "libc", 590 | "redox_syscall", 591 | "remove_dir_all", 592 | "winapi", 593 | ] 594 | 595 | [[package]] 596 | name = "time" 597 | version = "0.1.43" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 600 | dependencies = [ 601 | "libc", 602 | "winapi", 603 | ] 604 | 605 | [[package]] 606 | name = "tui" 607 | version = "0.14.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9" 610 | dependencies = [ 611 | "bitflags", 612 | "cassowary", 613 | "crossterm 0.18.2", 614 | "serde", 615 | "unicode-segmentation", 616 | "unicode-width", 617 | ] 618 | 619 | [[package]] 620 | name = "unicase" 621 | version = "2.6.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 624 | dependencies = [ 625 | "version_check", 626 | ] 627 | 628 | [[package]] 629 | name = "unicode-segmentation" 630 | version = "1.9.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" 633 | 634 | [[package]] 635 | name = "unicode-width" 636 | version = "0.1.9" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 639 | 640 | [[package]] 641 | name = "unicode-xid" 642 | version = "0.2.2" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 645 | 646 | [[package]] 647 | name = "vcpkg" 648 | version = "0.2.15" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 651 | 652 | [[package]] 653 | name = "version_check" 654 | version = "0.9.4" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 657 | 658 | [[package]] 659 | name = "wasi" 660 | version = "0.10.2+wasi-snapshot-preview1" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 663 | 664 | [[package]] 665 | name = "winapi" 666 | version = "0.3.9" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 669 | dependencies = [ 670 | "winapi-i686-pc-windows-gnu", 671 | "winapi-x86_64-pc-windows-gnu", 672 | ] 673 | 674 | [[package]] 675 | name = "winapi-i686-pc-windows-gnu" 676 | version = "0.4.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 679 | 680 | [[package]] 681 | name = "winapi-x86_64-pc-windows-gnu" 682 | version = "0.4.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 685 | 686 | [[package]] 687 | name = "windows-sys" 688 | version = "0.32.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" 691 | dependencies = [ 692 | "windows_aarch64_msvc", 693 | "windows_i686_gnu", 694 | "windows_i686_msvc", 695 | "windows_x86_64_gnu", 696 | "windows_x86_64_msvc", 697 | ] 698 | 699 | [[package]] 700 | name = "windows_aarch64_msvc" 701 | version = "0.32.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" 704 | 705 | [[package]] 706 | name = "windows_i686_gnu" 707 | version = "0.32.0" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" 710 | 711 | [[package]] 712 | name = "windows_i686_msvc" 713 | version = "0.32.0" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" 716 | 717 | [[package]] 718 | name = "windows_x86_64_gnu" 719 | version = "0.32.0" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" 722 | 723 | [[package]] 724 | name = "windows_x86_64_msvc" 725 | version = "0.32.0" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" 728 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stock" 3 | version = "1.4.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | #Log和SimpleLogger在TUI应用里看不到 8 | #log = "0.4" 9 | #simple_logger = "1.16" 10 | 11 | #crossterm支持windows,但性能比termion稍差,需要最新0.23版本,否则鼠标支持有问题 12 | tui = { version = "0.14", default-features = false, features = ['crossterm', 'serde'] } 13 | crossterm = { version = "0.23", features = [ "serde" ] } 14 | 15 | serde = {version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | chrono = "0.4" 18 | 19 | # 解决tui里中文宽度的计算 20 | unicode-width = "0.1" 21 | 22 | # reqwest太大了3M, ureq也有2M, http_req只有300k 23 | http_req = "0.8" 24 | 25 | # 查询跨平台的通用目录位置 26 | dirs-next = "2.0" 27 | 28 | #lazy_static = "1.4.0" 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 使用Rust开发的股价查询终端应用: 2 | 3 | ![](images/screen.gif) 4 | 5 | ## 编译/运行 6 | ``` 7 | cargo build 8 | cargo run 9 | ``` 10 | 11 | 12 | ## License 13 | 14 | Copyright (c) 2017-present x1y9 15 | 16 | [MIT License](http://en.wikipedia.org/wiki/MIT_License) -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @setlocal enableDelayedExpansion 2 | @REM set LANG for grep run correctly 3 | @set LANG=zh_CN.UTF-8 4 | 5 | @if "%1"=="" goto do_help 6 | @if "%1"=="clean" goto do_clean 7 | @if "%1"=="version" goto do_version 8 | @if "%1"=="debug" goto do_debug 9 | @if "%1"=="release" goto do_release 10 | @if "%1"=="publish" goto do_publish 11 | 12 | :do_help 13 | @echo build script for android project 14 | @echo. 15 | @echo Build: build [debug^|release^|publish] 16 | @echo Clean: build clean 17 | @echo Version: build version [number] 18 | @goto end 19 | 20 | :do_debug 21 | cargo run 22 | @IF %ERRORLEVEL% NEQ 0 goto error_end 23 | @call toast debug-run 24 | @goto end 25 | 26 | :do_release 27 | cargo run --release 28 | @IF %ERRORLEVEL% NEQ 0 goto error_end 29 | @call toast release-run 30 | @goto end 31 | 32 | :do_publish 33 | @echo check uncommit files... 34 | @git diff-files --quiet 35 | @IF %ERRORLEVEL% NEQ 0 goto error_end 36 | cargo build --release 37 | @IF %ERRORLEVEL% NEQ 0 goto error_end 38 | for /f %%i in ('grep -m 1 -oP "name = ""\K([a-zA-Z0-9.]+)" Cargo.toml') do set PACKAGE=%%i 39 | for /f %%i in ('grep -m 1 -oP "version = ""\K([0-9.]+)" Cargo.toml') do set VERSION=%%i 40 | for /f %%i in ('git rev-parse --short HEAD') do set HASH=%%i 41 | @echo publish to %PACKAGE%-%VERSION%-%HASH%.exe 42 | copy target\release\%PACKAGE%.exe %PACKAGE%-%VERSION%-%HASH%.exe 43 | @call toast publish-build 44 | @goto end 45 | 46 | :do_clean 47 | cargo clean 48 | @goto end 49 | 50 | :do_version 51 | @if "%2"=="" ( 52 | @grep -m 1 -oP "version = ""\K([0-9.]+)" Cargo.toml 53 | ) else ( 54 | sed -i -E "s/version = ""([0-9.]+)""$/version = ""%2""/" Cargo.toml 55 | cargo build --release 56 | @IF %ERRORLEVEL% NEQ 0 goto error_end 57 | git commit -m "%2" -a 58 | ) 59 | @goto end 60 | 61 | :error_end 62 | @call toast build-fail 63 | @echo Oops... Something wrong! 64 | @ver /ERROR >NUL 2>&1 65 | 66 | :end -------------------------------------------------------------------------------- /images/screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x1y9/rust-stock/e6bc59398e6b31b700eb6d9e484cc460d109ac70/images/screen.gif -------------------------------------------------------------------------------- /src/aio.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::mpsc::{Sender, channel}}; 2 | 3 | #[derive(Clone)] 4 | pub struct Executor { 5 | task_sender: Sender, 6 | } 7 | pub enum Task { 8 | Println(String), 9 | Exit, 10 | 11 | } 12 | 13 | impl Executor { 14 | pub fn new() -> Self { 15 | let (sender, receiver) = channel(); 16 | std::thread::spawn(move || { 17 | loop { 18 | match receiver.recv() { 19 | Ok(task) => { 20 | match task { 21 | Task::Println(string) => println!("{}", string), 22 | Task::Exit => return 23 | } 24 | }, 25 | Err(_) => { 26 | return; 27 | } 28 | } 29 | } 30 | }); 31 | Executor { task_sender: sender } 32 | } 33 | 34 | pub fn println(&self, string: String) { 35 | self.task_sender.send(Task::Println(string)).unwrap() 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, Event, MouseEventKind}; 2 | 3 | use crate::{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 | match app.state { 11 | AppState::Normal => { 12 | if let Event::Key(key) = event { 13 | let code = key.code; 14 | if code == KeyCode::Char('q') { 15 | app.should_exit = true; 16 | } 17 | else if code == KeyCode::Char('r') { 18 | app.refresh_stocks(); 19 | } 20 | else if code == KeyCode::Char('n') { 21 | //新建stock 22 | app.state = AppState::Adding; 23 | app.input = String::new(); 24 | } 25 | else if code == KeyCode::Char('d') && selsome { 26 | //删除当前选中的stock 27 | app.stocks.lock().unwrap().remove(sel); 28 | app.save_stocks().unwrap(); 29 | app.stocks_state.select(None); 30 | } 31 | else if code == KeyCode::Char('u') && selsome && sel > 0 { 32 | //将选中stock往上移动一位 33 | app.stocks.lock().unwrap().swap(sel, sel -1); 34 | app.save_stocks().unwrap(); 35 | app.stocks_state.select(Some(sel - 1)); 36 | } 37 | else if code == KeyCode::Char('j') && selsome && sel < total - 1 { 38 | //将选中stock往下移动一位 39 | app.stocks.lock().unwrap().swap(sel, sel + 1); 40 | app.save_stocks().unwrap(); 41 | app.stocks_state.select(Some(sel + 1)); 42 | } 43 | else if code == KeyCode::Up && total > 0 { 44 | //注意这里如果不加判断直接用sel - 1, 在sel为0时会导致异常 45 | app.stocks_state.select(Some(if sel > 0 {sel - 1} else {0})); 46 | } 47 | else if code == KeyCode::Down && total > 0 { 48 | app.stocks_state.select(Some(if sel < total - 1 {sel + 1} else {sel})); 49 | } 50 | } 51 | else if let Event::Mouse(mouse) = event { 52 | if let MouseEventKind::Up(_button) = mouse.kind { 53 | let row = mouse.row as usize; 54 | //list是从第三行开始,所以要减去2, 这里本来还应该考虑list的滚动, 55 | // 但是app.stocks_state的滚动位置字段是private的,取不到。 56 | if row >= 2 && row < total + 2{ 57 | app.stocks_state.select(Some(row - 2)); 58 | } 59 | } 60 | } 61 | }, 62 | 63 | AppState::Adding => match event { 64 | Event::Key(key) => match key.code { 65 | KeyCode::Enter => { 66 | app.state = AppState::Normal; 67 | if app.input.len() > 0 { 68 | app.stocks.lock().unwrap().push(Stock::new(&app.input)); 69 | app.refresh_stocks(); 70 | app.save_stocks().unwrap(); 71 | } 72 | } 73 | KeyCode::Esc => { 74 | app.state = AppState::Normal; 75 | } 76 | KeyCode::Char(c) => { 77 | app.input.push(c); 78 | } 79 | KeyCode::Backspace => { 80 | app.input.pop(); 81 | } 82 | _ => {} 83 | }, 84 | _ => {}, 85 | } 86 | } 87 | } 88 | 89 | //处理定时事件 90 | pub fn on_tick(app:&mut App) { 91 | app.tick_count+=1; 92 | if app.tick_count % 60 == 0 { 93 | if let AppState::Normal = app.state { 94 | app.refresh_stocks(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Stdout, fs, collections::HashMap, sync::{Mutex, Arc}, thread}; 2 | 3 | use chrono::{DateTime, Local}; 4 | use http_req::request; 5 | use serde::{Serialize, Deserialize}; 6 | use serde_json::{Value, Map, json}; 7 | use tui::{backend::CrosstermBackend, widgets::ListState}; 8 | 9 | pub mod events; 10 | pub mod widget; 11 | pub mod aio; 12 | 13 | pub type DynResult = Result<(), Box>; 14 | pub type CrossTerminal = tui::Terminal>; 15 | pub type TerminalFrame<'a> = tui::Frame<'a, CrosstermBackend>; 16 | 17 | pub const DB_PATH: &str=".stocks.json"; 18 | 19 | #[derive(Serialize, Deserialize, Clone, Debug)] 20 | pub struct Stock { 21 | pub title: String, 22 | pub code: String, 23 | pub price: f64, 24 | pub percent: f64, 25 | pub open: f64, //今开 26 | pub yestclose: f64, //昨收 27 | pub high: f64, //最高 28 | pub low: f64, //最低 29 | //pub slice: Vec 30 | } 31 | 32 | impl Stock { 33 | pub fn new(code:&String) ->Self { 34 | Self { 35 | code: code.clone(), 36 | title: code.clone(), 37 | price:0.0, 38 | percent:0.0, 39 | open:0.0, 40 | yestclose:0.0, 41 | high:0.0, 42 | low:0.0, 43 | //slice:vec![], 44 | } 45 | } 46 | } 47 | 48 | pub enum AppState { 49 | Normal, 50 | Adding, 51 | } 52 | pub struct App { 53 | pub should_exit:bool, 54 | pub state:AppState, 55 | pub error:Arc>, 56 | pub input:String, 57 | pub stocks:Arc>>, 58 | //TUI的List控件需要这个state记录当前选中和滚动位置两个状态 59 | pub stocks_state:ListState, 60 | pub last_refresh:Arc>>, 61 | pub tick_count:u128, 62 | } 63 | 64 | impl App { 65 | pub fn new() -> Self { 66 | let mut app = Self { 67 | should_exit: false, 68 | state: AppState::Normal, 69 | input: String::new(), 70 | error: Arc::new(Mutex::new(String::new())), 71 | stocks: Arc::new(Mutex::new([].to_vec())), 72 | //ListState:default为未选择,因为可能stocks为空,所以不能自动选第一个 73 | stocks_state: ListState::default(), 74 | last_refresh: Arc::new(Mutex::new(Local::now())), 75 | tick_count: 0, 76 | }; 77 | app.load_stocks().unwrap_or_default(); 78 | app.refresh_stocks(); 79 | return app; 80 | } 81 | 82 | pub fn save_stocks(&self) -> DynResult{ 83 | let db=dirs_next::home_dir().unwrap().join(DB_PATH); 84 | //每个stock单独存一个对象,是考虑将来的扩展性 85 | let stocks = self.stocks.lock().unwrap(); 86 | let lists:Vec<_> = stocks.iter().map(|s| HashMap::from([("code", &s.code)])).collect(); 87 | fs::write(&db, serde_json::to_string(&HashMap::from([("stocks", lists)]))?)?; 88 | Ok(()) 89 | } 90 | 91 | pub fn load_stocks(&mut self) -> DynResult{ 92 | //用unwrap_or_default屏蔽文件不存在时的异常 93 | let content = fs::read_to_string(dirs_next::home_dir().unwrap().join(DB_PATH)).unwrap_or_default(); 94 | //如果直接转换stocks,必须所有key都对上, 兼容性不好 95 | //self.stocks = serde_json::from_str(&content).unwrap_or_default(); 96 | 97 | //先读成Map再转换,可以增加兼容性, 98 | let json: Map = serde_json::from_str(&content).unwrap_or_default(); 99 | let mut data = self.stocks.lock().unwrap(); 100 | data.clear(); 101 | data.append(&mut json.get("stocks").unwrap_or(&json!([])).as_array().unwrap().iter() 102 | .map(|s| Stock::new(&s.as_object().unwrap().get("code").unwrap().as_str().unwrap().to_string())) 103 | .collect()); 104 | 105 | Ok(()) 106 | } 107 | 108 | pub fn refresh_stocks(&mut self) { 109 | let stock_clone = self.stocks.clone(); 110 | let err_clone = self.error.clone(); 111 | let last_refresh_clone = self.last_refresh.clone(); 112 | let codes = self.get_codes(); 113 | if codes.len() > 0 { 114 | thread::spawn(move || { 115 | let mut writer = Vec::new(); 116 | let ret = request::get(format!("{}{}","http://api.money.126.net/data/feed/", codes), &mut writer); 117 | let mut locked_err = err_clone.lock().unwrap(); 118 | if let Err(err) = ret { 119 | *locked_err = format!("{:?}", err); 120 | } 121 | else { 122 | let content = String::from_utf8_lossy(&writer); 123 | if content.starts_with("_ntes_quote_callback") { 124 | let mut stocks = stock_clone.lock().unwrap(); 125 | //网易的返回包了一个js call,用skip,take,collect实现一个substring剥掉它 126 | let json: Map = serde_json::from_str(&content.chars().skip(21).take(content.len() - 23).collect::()).unwrap(); 127 | for stock in stocks.iter_mut() { 128 | //如果code不对,返回的json里不包括这个对象, 用unwrap_or生成一个空对象,防止异常 129 | let obj = json.get(&stock.code).unwrap_or(&json!({})).as_object().unwrap().to_owned(); 130 | stock.title = obj.get("name").unwrap_or(&json!(stock.code.clone())).as_str().unwrap().to_owned(); 131 | stock.price = obj.get("price").unwrap_or(&json!(0.0)).as_f64().unwrap(); 132 | stock.percent = obj.get("percent").unwrap_or(&json!(0.0)).as_f64().unwrap(); 133 | stock.open = obj.get("open").unwrap_or(&json!(0.0)).as_f64().unwrap(); 134 | stock.yestclose = obj.get("yestclose").unwrap_or(&json!(0.0)).as_f64().unwrap(); 135 | stock.high = obj.get("high").unwrap_or(&json!(0.0)).as_f64().unwrap(); 136 | stock.low = obj.get("low").unwrap_or(&json!(0.0)).as_f64().unwrap(); 137 | 138 | // if json.contains_key(&stock.code) { 139 | // let mut writer2 = Vec::new(); 140 | // request::get(format!("http://img1.money.126.net/data/hs/time/today/{}.json",stock.code), &mut writer2)?; 141 | // println!("{:?}", format!("http://img1.money.126.net/data/hs/time/today/{}.json",stock.code)); 142 | // let json2: Map = serde_json::from_str(&String::from_utf8_lossy(&writer2).to_string())?; 143 | // stock.slice = json2.get("data").unwrap().as_array().unwrap() 144 | // .iter().map(|item| item.as_array().unwrap().get(2).unwrap().as_f64().unwrap()) 145 | // .collect(); 146 | // } 147 | } 148 | let mut last_refresh = last_refresh_clone.lock().unwrap(); 149 | *last_refresh = Local::now(); 150 | *locked_err = String::new(); 151 | } 152 | else { 153 | *locked_err = String::from("服务器返回错误"); 154 | } 155 | } 156 | }); 157 | } 158 | } 159 | 160 | pub fn get_codes(&self) -> String { 161 | let codes:Vec = self.stocks.lock().unwrap() 162 | .iter() 163 | .map(|stock| stock.code.clone()) 164 | .collect(); 165 | codes.join(",") 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, time::{Instant, Duration}}; 2 | 3 | use stock::{DynResult, CrossTerminal, App, TerminalFrame, events, widget, AppState}; 4 | use tui::{Terminal, backend::CrosstermBackend, widgets}; 5 | use unicode_width::UnicodeWidthStr; 6 | 7 | 8 | fn main() -> DynResult{ 9 | let mut app = App::new(); 10 | let mut terminal = init_terminal()?; 11 | main_loop(&mut terminal, &mut app)?; 12 | close_terminal(terminal)?; 13 | 14 | Ok(()) 15 | } 16 | 17 | fn init_terminal() -> Result> { 18 | let mut stdout = std::io::stdout(); 19 | crossterm::terminal::enable_raw_mode()?; 20 | //必须先执行EnableMouseCapture后面才能支持鼠标事件 21 | crossterm::execute!(stdout, crossterm::terminal::EnterAlternateScreen, crossterm::event::EnableMouseCapture)?; 22 | let backend = CrosstermBackend::new(stdout); 23 | let mut terminal = Terminal::new(backend)?; 24 | terminal.clear()?; 25 | Ok(terminal) 26 | } 27 | 28 | fn close_terminal(mut terminal: CrossTerminal) -> DynResult{ 29 | crossterm::terminal::disable_raw_mode()?; 30 | crossterm::execute!(terminal.backend_mut(), crossterm::event::DisableMouseCapture, crossterm::terminal::LeaveAlternateScreen)?; 31 | Ok(()) 32 | } 33 | 34 | //主事件循环 35 | fn main_loop(terminal: &mut CrossTerminal, app: &mut App) -> DynResult { 36 | let mut last_tick = Instant::now(); 37 | while !app.should_exit { 38 | terminal.draw(|f| {on_draw(f, app);})?; 39 | 40 | if crossterm::event::poll(Duration::from_secs(1).checked_sub(last_tick.elapsed()).unwrap_or_default())? { 41 | events::on_events(crossterm::event::read()?, app); 42 | } 43 | else { 44 | events::on_tick(app); 45 | last_tick = Instant::now(); 46 | } 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | fn on_draw(frame: &mut TerminalFrame, app: &mut App) { 53 | let chunks = widget::main_chunks(frame.size()); 54 | 55 | //list的render需要调render_stateful_widget,否则滚动状态不对,这里第一个参数不能是app,否则会和后面的mut stock_state冲突 56 | frame.render_stateful_widget(widget::stock_list(&app.stocks.lock().unwrap()), chunks[1], &mut app.stocks_state); 57 | //因为render stock_list时会修改滚动状态,后面如果要用到这个值,就需要先做list的render 58 | frame.render_widget(widget::title_bar(app, frame.size()), chunks[0]); 59 | frame.render_widget(widget::stock_detail(app), chunks[2]); 60 | frame.render_widget(widget::status_bar(app), chunks[3]); 61 | 62 | if let AppState::Adding = app.state { 63 | //popup需要先clear一下,否则下面的背景色会透上来 64 | frame.render_widget(widgets::Clear, chunks[4]); 65 | frame.render_widget(widget::stock_input(app), chunks[4]); 66 | 67 | //显示光标, width()接口依赖一个外部包,可以正确处理中文宽度 68 | frame.set_cursor(chunks[4].x + app.input.width() as u16 + 1, chunks[4].y + 1); 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/widget.rs: -------------------------------------------------------------------------------- 1 | use tui::{layout::{Rect, Layout, Direction, Constraint, Alignment}, 2 | widgets::{Paragraph, Block, Borders, BorderType, List, ListItem}, 3 | style::{Style, Color, Modifier}, text::{Spans, Span}}; 4 | 5 | use crate::{App, Stock, AppState}; 6 | use unicode_width::UnicodeWidthStr; 7 | 8 | 9 | const VERSION:&str = env!("CARGO_PKG_VERSION"); 10 | 11 | //计算所有的屏幕窗口区域,供后续render使用 12 | pub fn main_chunks(area: Rect) -> Vec { 13 | let parent = Layout::default() 14 | .direction(Direction::Vertical) 15 | .constraints([ 16 | Constraint::Length(1), 17 | Constraint::Min(1), 18 | Constraint::Length(1), 19 | ].as_ref()) 20 | .split(area); 21 | 22 | let center = Layout::default() 23 | .direction(Direction::Horizontal) 24 | .margin(0) 25 | .constraints([ 26 | Constraint::Percentage(30), 27 | Constraint::Percentage(70), 28 | ].as_ref()) 29 | .split(parent[1]); 30 | 31 | //计算新建stock时的弹框位置 32 | let popup = Layout::default() 33 | .direction(Direction::Vertical) 34 | .constraints([ 35 | Constraint::Percentage(40), 36 | Constraint::Length(3), 37 | Constraint::Percentage(40), 38 | ].as_ref()) 39 | .split(area); 40 | 41 | let popline = Layout::default() 42 | .direction(Direction::Horizontal) 43 | .margin(0) 44 | .constraints([ 45 | Constraint::Percentage(10), 46 | Constraint::Percentage(80), 47 | Constraint::Percentage(10), 48 | ].as_ref()) 49 | .split(popup[1]); 50 | 51 | vec!(parent[0], center[0], center[1], parent[2], popline[1]) 52 | } 53 | 54 | pub fn stock_list(stocks: &Vec) -> List { 55 | let items: Vec<_> = stocks.iter() 56 | .map(|stock| { 57 | ListItem::new(Spans::from(vec![ 58 | Span::styled(format!("{:+.2}% ",stock.percent * 100.0), 59 | Style::default().fg(if stock.percent < 0.0 {Color::Green} else {Color::Red})), 60 | Span::styled(stock.title.clone(),Style::default()), 61 | ])) 62 | }).collect(); 63 | 64 | List::new(items) 65 | .block( 66 | Block::default() 67 | .borders(Borders::ALL) 68 | .style(Style::default().fg(Color::White)) 69 | .title("列表") 70 | .border_type(BorderType::Plain)) 71 | .highlight_style( 72 | Style::default() 73 | .bg(Color::Yellow) 74 | .fg(Color::Black) 75 | .add_modifier(Modifier::BOLD)) 76 | } 77 | 78 | pub fn stock_detail(app: &App) -> Paragraph { 79 | let mut info = String::new(); 80 | let sel = app.stocks_state.selected().unwrap_or(0); 81 | //这里要防止sel超出列表范围 82 | let stocks = app.stocks.lock().unwrap(); 83 | if app.stocks_state.selected().is_some() && sel < stocks.len() { 84 | let stock = stocks.get(sel).unwrap(); 85 | info = format!("代码:{}\n涨跌:{:+.2}%\n当前:{}\n今开:{}\n昨收:{}\n最高:{}\n最低:{}", 86 | stock.code, stock.percent * 100.0, stock.price, stock.open, stock.yestclose, stock.high, stock.low); 87 | } 88 | 89 | Paragraph::new(info) 90 | .alignment(Alignment::Center) 91 | .style(Style::default()) 92 | .block(Block::default().title("详情") 93 | .borders(Borders::ALL) 94 | .border_type(BorderType::Plain)) 95 | } 96 | 97 | pub fn stock_input(app: &App) -> Paragraph { 98 | Paragraph::new(app.input.as_ref()) 99 | .style(Style::default().fg(Color::Yellow)) 100 | .block(Block::default().borders(Borders::ALL).title("输入证券代码")) 101 | } 102 | 103 | pub fn title_bar(app: &App, rect: Rect) -> Paragraph { 104 | let left = format!("Stock v{}", VERSION); 105 | let error = app.error.lock().unwrap(); 106 | let right = if error.is_empty() { app.last_refresh.lock().unwrap().format("最后更新 %H:%M:%S").to_string() } else { error.clone() }; 107 | Paragraph::new(Spans::from(vec![ 108 | Span::raw(left.clone()), 109 | //使用checked_sub防止溢出 110 | Span::raw(" ".repeat((rect.width as usize).checked_sub(right.width() + left.width()).unwrap_or(0))), 111 | Span::styled(right,Style::default() 112 | .fg(if error.is_empty() { Color::White } else { Color::Red })), 113 | ])) 114 | .alignment(Alignment::Left) 115 | } 116 | 117 | pub fn status_bar(app: &mut App) -> Paragraph { 118 | Paragraph::new(match app.state { 119 | AppState::Normal => "退出[Q] | 新建[N] | 删除[D] | 刷新[R] | 上移[U] | 下移[J]", 120 | AppState::Adding => "确认[Enter] | 取消[ESC] | 上交所代码前需要加0,深市加1" 121 | }.to_string() 122 | ).alignment(Alignment::Left) 123 | } 124 | --------------------------------------------------------------------------------