跪拜 Guibai
← Back to the summary

BossKey-Stock Puts Live Market Data in Your Terminal, Then Disguises It as a Docker Build

Here's how it happened.

One afternoon at 3 PM, I switched to my stock app for the fifth time, glanced at it, and switched back to coding. The motion was smooth, under a second. But the colleague at the next desk still noticed.

"How much did you lose?"

I said I didn't lose any. He didn't believe me. With a screen full of red and green, who would?

But the real problem isn't profit or loss — it's that the act of watching the market is public. Every time you switch windows, every time you stare at those red and green numbers, you're telling everyone: I'm not working right now. Even if you really did just glance and go back to fixing a bug, that feeling of "getting caught" is awful.

I work in the terminal. Eight hours a day, either in Vim or in tmux. My thought at the time was: if I could see market data inside the terminal, anyone walking by would only see code, and I wouldn't have to keep switching windows.

I looked around. Existing solutions all felt wrong:

So I started writing.

Technology choices

The requirements were clear:

  1. Runs in the terminal, no browser.
  2. No API key, no registration.
  3. When someone walks by, they can't tell at a glance you're looking at stocks.
  4. Installation as simple as possible.

Point three is the most important. Plain text isn't enough — if it says "Kweichow Moutai 1690.00 +0.90%", anyone who knows will see what you're doing in one look. There had to be a one-key disguise mode.

So the final stack is:

The whole project is called BossKey-Stock. The BossKey (boss key) is the core feature.

Architecture

Not much code, 500 lines, split into five modules:

bosskey_stock/
├── __main__.py    CLI entry point
├── app.py         Main loop + terminal control
├── data.py        Sina data source
├── boss.py        Boss mode disguise
└── config.py      Config read/write

The CLI layer is nothing special — argparse routes a few subcommands. Config read/write uses tomlkit to read/write ~/.bosskey.toml.

The interesting parts are terminal control and boss mode.

Rich Live

Rich comes with a Live component that maintains a "dynamic area" in the terminal, with content that updates in real time. Under the hood it uses the alternate screen buffer — the same mode vim and top use, where the terminal restores its previous content after the program exits.

Typical usage:

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

Written this way, the program is frozen during the refresh interval and can't handle keyboard input. So it needs some reworking.

Non-blocking keyboard input

This is where I hit the most snags.

Why not input()?

input() is line-buffered — it waits until the user presses Enter before returning. But I needed single-key response: press b to switch modes instantly, press q to quit instantly, no waiting for Enter.

Why not curses?

curses can do it, but it takes over the entire terminal and conflicts with Rich's Live component. Rich relies on low-level terminal control for colors and rendering; once curses steps in, everything breaks. Two libraries fighting over terminal control, neither yielding.

The solution: operate termios directly

Python's standard library provides termios and select, which allow precise control over terminal behavior.

The core idea: only change input configuration, leave the output side alone.

def _setup_tty(fd):
    attrs = termios.tcgetattr(fd)
    # Input side: turn off ICANON (per-key mode), ECHO (no echo), ISIG (no signals)
    attrs[3] &= ~(termios.ECHO | termios.ICANON | termios.ISIG)
    # Output side: don't touch, keep OPOST to ensure \n → \r\n conversion works
    termios.tcsetattr(fd, termios.TCSAFLUSH, attrs)

After this change, os.read(fd, 1) can read a single keypress, no Enter needed.

Then the main loop uses select for non-blocking checking:

def _read_key(fd):
    r, _, _ = select.select([fd], [], [], 0.5)  # wait at most 0.5 seconds
    if r:
        return os.read(fd, 1).decode("utf-8", errors="replace")
    return None

A detail here: select's timeout is set to 0.5 seconds, rather than using non-blocking mode. The reason is that the main loop also needs to do timed refreshes (pull market data every 3 seconds), so it can't truly block on read. When select returns due to timeout (no keypress), execution continues to check whether data needs refreshing. If the user pressed a key, it handles the key logic.

This way the entire main loop runs in a single thread: keyboard listening + timed refresh + UI rendering. No background threads, no locks.

An easy pitfall: ICRNL

During debugging I found a strange behavior: the Enter key (\r) was being converted to a newline (\n), causing key detection errors. This only happened on certain terminal emulators.

I looked it up — it's the ICRNL flag in termios causing trouble. This flag automatically converts received \r into \n. My solution was to turn it off:

attrs[0] &= ~(termios.ICRNL | termios.IXON)  # ICRNL is the key one

Boss mode

The implementation of boss mode is actually simple. Pressing b toggles a boolean, then renders different content:

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

boss.render() generates a simulated Docker build log. The core logic randomly picks some steps and assembles output that looks like a real build.

To make the log more convincing, I added some details:

All this logic lives in the BossGenerator class, 70 lines of code, with a pre-generated log pool repeated 5 times and looped. The effect is decent — I showed a colleague at work and he said, "Messing with Docker again?"

I said yeah, always messing with it.

Data layer

The data source is Sina Finance's market data endpoint. No API key needed, just one HTTP request:

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

The response is a JavaScript variable assignment, GBK-encoded:

var hq_str_sh600519="Kweichow Moutai,1680.00,1675.00,1690.00,1700.00,1670.00,...";

Parsing isn't complicated — split by comma, grab the needed fields. But there are a few points worth noting:

1. Code prefix conversion. Codes starting with 6 or 9 are Shanghai Stock Exchange (sh prefix); the rest are Shenzhen Stock Exchange (sz prefix). The regex to extract the code from the response line uses the pattern hq_str_[a-z]+(\d+)=".

2. Volume formatting. The volume Sina returns is in shares (not lots). In the A-share market, one lot equals 100 shares. Display should convert to lots:

if lots >= 100_000_000:  # over 100 million lots
    return f"{lots / 100_000_000:.2f}亿"
if lots >= 10_000:        # over 10,000 lots
    return f"{lots / 10_000:.2f}万"
return f"{lots:.0f}"

3. Trading session detection. No point fetching data on non-trading days or during breaks. The logic: Monday through Friday, 9:30–11:30 or 13:00–15:00. Outside trading hours, display [After Hours] and show the data source's last trade time.

4. Offline handling. When a network request times out or throws an exception, $requests$ doesn't raise — it returns None. The main loop detects None, displays a yellow [Offline] indicator, and keeps the last fetched data rather than clearing the table.

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

Beyond slacking off

Back to the original problem.

What this tool solves isn't really "being able to see stocks" — it's that the act of looking at stocks doesn't impose psychological pressure on others.

The concept of a boss key is actually quite old — it's been around since the DOS era, pressing F1 to turn a game screen into a legitimate Lotus 1-2-3 spreadsheet. What I built just turns the terminal into a disguise tool.

Technically there's nothing particularly advanced here. But the process made me realize one thing: many demands that seem like "slacking off" are essentially a discomfort with the current environment. A programmer works in the terminal for 8 hours and occasionally wants to glance at the market — that's perfectly normal. What's abnormal is that this behavior carries a negative social signal in the office — "he's not working hard."

A tool can't solve that problem, but at least it spares people a little embarrassment.


Project repo: github.com/Angryshark128/bosskey-stock
Installation: pip install bosskey-stock, then run bosskey. Requires Python 3.10+.

The terminal control code mentioned in the article is in app.py and config.py. Take a look if you're interested.

Comments

Top 1 of 3 from juejin.cn, machine-translated. The original thread is authoritative.

向新出发叭

Haha this is brilliant, the need to 'press the b key to pretend you're compiling' is practically a hard requirement 😂 I used to sneak peeks at the market in my browser too, but switching windows was way too obvious. After my boss caught me twice, I learned my lesson. Your approach of using termios + select for non-blocking monitoring is clever, much lighter than curses, and Rich's table rendering really does look good. I've also stepped on that GBK decoding pitfall; Sina's API returns data with inconsistent encoding. A small suggestion: if you want to support Hong Kong or US stocks later, you could try akshare as a data source. Its API stability is better than Sina's, and you wouldn't have to handle encoding issues yourself. Also, the choice of TOML for configuration is great, much more readable than JSON and it supports comments. Starred, looking forward to a custom stock grouping feature later~

寒蝉128

Haha, same feeling. Just added a position cost/profit feature, and more features will be added later [grin]

寒蝉128

I also considered using akshare, but I haven't seen stability issues with Sina yet. Supporting multiple data source rotations could be an option later~