我写了个终端盯盘工具,按一下 b 键就能假装在编译代码

0 阅读7分钟

事情是这样的。

某天下午三点,我第五次切到同花顺看了一眼,又切回来继续写代码。动作很流畅,不到一秒。但隔壁工位的同事还是注意到了。

"你股票亏了多少?"

我说没亏。他不信。因为屏幕上一片红绿,你说没亏谁信啊。

但其实问题不是亏不亏,而是盯盘这件事本身是公开的。你每一次切屏、每一次盯着红绿数字看,都等于在告诉别人——我现在没在工作。哪怕你确实只是扫了一眼就回来改 bug 了,那种"被抓到"的感觉也很不爽。

我在终端里工作。一天八个小时,不是在 Vim 里就是在 tmux 里。我当时想的是:如果能在终端里看行情就好了,别人路过看到的全是代码,我也用不着来回切窗口了。

翻了翻,已有的方案都不太对劲:

  • tushare/baostock:要注册、要 token、数据接口是给量化分析用的,不是给人盯盘用的。
  • 同花顺/东方财富的 Web 版:一个浏览器标签页在那摆着,标题就写着"行情",掩耳盗铃。
  • curl 请求然后 grep:能用,但这个体验,讲真,你盯不了三分钟。

于是我开始写了。

选型

需求很明确:

  1. 终端里跑,不要开浏览器
  2. 不要 API Key,不要注册
  3. 别人路过看一眼时,不能一眼看出是在看股票
  4. 安装尽量简单

第三点是最重要的。纯文本还不够——要是上面写着"贵州茅台 1690.00 +0.90%",懂行的看一眼就知道你在干嘛。必须有一个一键切换伪装模式的功能。

所以最后的选型是:

  • Python 3.10+ 标准库 + 三个第三方包
  • Rich 做终端 UI(Live + Table)
  • Sina Finance API 做数据源(免费的,不需要 Key)
  • TOML 做配置文件

整个项目叫 BossKey-Stock。这个 BossKey(老板键)才是核心功能。

架构

代码不多,500 行,拆了五个模块:

bosskey_stock/
├── __main__.py    CLI 入口
├── app.py         主循环 + 终端控制
├── data.py        Sina 数据源
├── boss.py        老板模式伪装
└── config.py      配置读写

CLI 层没什么好说的,argparse 路由几个子命令。配置读写就是 tomlkit 读写 ~/.bosskey.toml

有意思的部分是终端控制和老板模式

Rich Live

Rich 自带一个 Live 组件,可以在终端里维护一个"动态区域",内容可以实时更新。实现上它用了 alternate screen buffer——就是 vimtop 那种模式,程序退出后终端会恢复到之前的内容。

典型用法就是:

with Live(auto_refresh=False, screen=True) as live:
    while True:
        live.update(build_table(), refresh=True)
        time.sleep(interval)

直接这样写的话,刷新间隔内程序是卡死的,没法处理键盘输入。所以需要一点改造。

非阻塞键盘输入

这也是踩坑最多的地方。

为什么不用 input()?

input() 是行缓冲的——它要等到用户按了回车才会返回。但我要的是单键响应:按 b 立刻切模式,按 q 立刻退出,不能等回车。

为什么不用 curses?

curses 可以做到,但它接管了整个终端,和 Rich 的 Live 组件有冲突。Rich 依赖底层的终端控制来做色彩和渲染,curses 一插手就乱套了。两个库抢终端控制权,谁都不让谁。

方案:直接操作 termios

Python 标准库提供了 termiosselect,可以精确控制终端的行为。

核心思路是:只改输入的配置,不改输出侧

def _setup_tty(fd):
    attrs = termios.tcgetattr(fd)
    # 输入侧:关掉 ICANON(逐键模式)、ECHO(不回显)、ISIG(不发信号)
    attrs[3] &= ~(termios.ECHO | termios.ICANON | termios.ISIG)
    # 输出侧:不动,保留 OPOST,确保 \n → \r\n 正常转换
    termios.tcsetattr(fd, termios.TCSAFLUSH, attrs)

改完之后,os.read(fd, 1) 就能读到单个按键了,不需要回车。

然后主循环里用 select 做非阻塞检查:

def _read_key(fd):
    r, _, _ = select.select([fd], [], [], 0.5)  # 最多等 0.5 秒
    if r:
        return os.read(fd, 1).decode("utf-8", errors="replace")
    return None

这里有个细节:select 的 timeout 设成 0.5 秒,而不是用非阻塞模式。原因是主循环里还要做定时刷新(每 3 秒拉一次行情),不能真的阻塞在 read 上。select 返回后如果是超时(没按键),就继续往下走,检查要不要刷新数据。如果用户按了键,就处理按键逻辑。

这样整个主循环就在一个线程里完成了:键盘监听 + 定时刷新 + UI 渲染。不需要后台线程,不需要锁。

一个容易踩的坑:ICRNL

在调试过程中发现一个奇怪的现象:回车键(\r)会被转换成换行(\n),导致按键检测出错。这个问题只在某些终端模拟器上出现。

查了一下,是 termiosICRNL 标志在搞鬼。这个标志会把收到的 \r 自动转成 \n。我的解决方案是关掉它:

attrs[0] &= ~(termios.ICRNL | termios.IXON)  # ICRNL 是关键

老板模式

老板模式的实现其实很简单。就是按 b 键时,切换一个布尔值,然后渲染不同的内容:

if boss_mode:
    live.update(boss.render(), refresh=True)
    continue

boss.render() 生成一段模拟的 Docker build 日志。核心逻辑就是随机选一些步骤,组合成看起来像正经构建的输出。

为了让日志更逼真,我加了一些细节:

  • Docker 的构建步骤编号
  • 偶尔的 WARNING 信息
  • Running time: MM:SS 的计时器
  • 日志每秒滚动两行(通过时间来偏移数组位置)

这些逻辑都在 BossGenerator 类里,70 行代码,有个预生成的日志池子,5 倍重复后循环滚动。效果还不错——我让公司另一个同事看了看,他说"你又在折腾 Docker?"

我说对,一直在折腾。

数据层

数据源是 Sina Finance 的行情接口。不需要 API Key,只要一个 HTTP 请求:

GET https://hq.sinajs.cn/list=sh600519,sz000001
Referer: https://finance.sina.com.cn

返回的是 JavaScript 变量赋值,GBK 编码:

var hq_str_sh600519="贵州茅台,1680.00,1675.00,1690.00,1700.00,1670.00,...";

解析逻辑不复杂,按逗号分割,取需要的字段。但有一些需要注意的点:

1. 代码前缀转换。 以 6 和 9 开头的代码是上交所(sh前缀),其他是深交所(sz前缀)。正则从响应行里提取代码时,用的是 hq_str_[a-z]+(\d+)=" 这个模式。

2. 成交量格式化。 Sina 返回的成交量单位是(不是手)。A 股一手等于 100 股。显示时要转成手为单位:

if lots >= 100_000_000:  # 一亿手以上
    return f"{lots / 100_000_000:.2f}亿"
if lots >= 10_000:        # 一万手以上
    return f"{lots / 10_000:.2f}万"
return f"{lots:.0f}"

3. 交易时段检测。 没必要在非交易日和休息时间刷数据。判断逻辑是:周一到周五,9:30-11:30 或 13:00-15:00。非交易时段显示 [After Hours] 并展示数据源的最后成交时间。

4. 离线处理。 网络请求超时或异常时,requestsrequests 不会抛异常,而是返回 None。主循环里检测到 None 会显示 [Offline] 黄色提示,并且保留上一次拉到的数据,而不是清空表格。

result = fetch(codes)
if result is None:
    offline = True
else:
    offline = False
    stocks_cache = result

摸鱼之外

回到最开始的问题。

这个工具解决的其实不是"能看股票",而是"看股票这件事不给别人带来心理压力"。

老板键这个概念其实很老了——从 DOS 时代就有,按一下 F1 把一个游戏界面变成一张正版的 Lotus 1-2-3 表格。我做的这个,只不过是把终端变成了伪装工具。

技术上没什么特别高深的。但这个过程让我想通了一件事:很多看似"偷懒"的需求,本质是对当前环境的一种不适应。程序员在终端里工作 8 小时,偶尔想看一眼行情,这是很正常的。不正常的是这个行为在办公室里带有负面的社会信号——"他不好好工作"。

工具解决不了这个问题,但至少让人少了一点尴尬。


项目地址: github.com/Angryshark1…
安装方式: pip install bosskey-stock,运行 bosskey 即可。需要 Python 3.10+。

文章里提到的终端控制代码在 app.pyconfig.py。有兴趣可以看看。