Vol. V · No. 12Shah Alam, SelangorSaturday, 31 October 2026Circulation 411

Full Stack

Rustman, Part 2: A Terminal, a Server Button, and Windows Being Windows

I asked for a terminal inside my Postman alternative. What I really wanted was to stop losing track of my local servers. Along the way: a pseudo-console that waits for an answer, keystrokes that arrive out of order, a test that passed for the wrong reason, and a password I pushed to GitLab.

In this article
  1. Cancel, in 23 milliseconds
  2. A terminal, like VS Code's
  3. What I actually needed: run tasks
  4. Importing from Postman
  5. Sharing a request as a Markdown file
  6. The password I pushed to GitLab
  7. Where Part 2 leaves me

In Part 1 Rustman learned to send requests, keep cookies and save responses. This part is about everything around sending: cancelling a slow request, a built-in terminal, run tasks, importing from Postman, and sharing a request as a file. Oh, and the password I pushed to GitLab. We'll get there.


Cancel, in 23 milliseconds

While a request is sending, Send turns into Cancel, and Esc works too.

In Rust, each send races against a one-shot "cancel" channel with tokio::select!. If the cancel wins, the unfinished send is dropped, and dropping it aborts the HTTP request. Against a local server that never answers, the request stopped 23 ms after I clicked Cancel.

Cancel while a slow request is sending


A terminal, like VS Code's

Ctrl+` opens a bottom panel with tabs for PowerShell, Command Prompt and Git Bash, starting in the workspace folder. Shells keep running while the panel is hidden. Under the hood it's portable-pty (Windows' ConPTY) in Rust, xterm.js in the UI, and Tauri events in between.

Sounds simple, right? Three things bit me.

1. bash.exe isn't the bash you think

On my machine, bash.exe on the PATH is C:\Windows\system32\bash.exe. That's WSL, not Git Bash. So Rustman now finds Git Bash next to git.exe (Git\cmd\git.exe → Git\bin\bash.exe), and a test makes sure the result isn't in System32.

2. The pseudo-console waits for an answer

The Rust tests that start cmd.exe kept timing out, and the shell never exited. Temporary debug prints showed the reader got exactly 4 bytes, then nothing.

Those 4 bytes were ESC[6n, which means "terminal, where is your cursor?" Windows' pseudo-console sends it at startup and doesn't start the program until the terminal replies. In the app, xterm.js answers automatically, so everything worked. The tests had no terminal attached, so nobody answered, and cmd never started. Imagine waiting forever for someone to say hi back. The tests now reply ESC[1;1R like a real terminal would.

One more Windows thing: the output stream doesn't end when the shell exits, only when the pseudo-console is closed. So a separate thread waits for the process itself to report its exit.

3. exit ran ixt

I typed exit in the app and got ixt: command not found. Yes, really.

Each keystroke was its own call from the UI to Rust, and Tauri can process those calls in parallel, so the letters raced each other. The fix is a small outbox per terminal: one write in flight at a time, and whatever you type in the meantime goes in the next write. A fast-typed 108-character line now arrives intact.

And the one that wasn't a bug

My feedback after testing: "The terminal works, but my PowerShell uses oh-my-posh, so the profile didn't carry over and it looks bad."

Turns out the profile had loaded. The prompt was right there. What was missing was the font. oh-my-posh draws its icons with a Nerd Font, my Windows Terminal uses RobotoMono Nerd Font, and Rustman's terminal used plain Cascadia Code, so the icons showed up as boxes. The terminal now tries common Nerd Fonts first. (I like my terminal pretty. Don't judge.)

PowerShell with an oh-my-posh prompt inside Rustman, icons working


What I actually needed: run tasks

When Claude asked what the terminal was for, I told it straight: "So I can run the local server from Rustman instead of opening my own terminal. On my laptop it's hard to keep track."

A terminal doesn't really solve that. You still have to remember which folder and which command, on every machine. So we built tasks, like VS Code's:

  • A Tasks tab. Each task (say, Backend → npm run dev) has a status dot (green running, grey stopped, red crashed) and a ▶ / ■ button.
  • A "1 task running" indicator, so you always know.
  • Start automatically when the workspace opens.
  • One file per task in tasks/, shared through git like everything else.
  • "This folder is only for this computer", the same idea as secret variables. The path goes into a gitignored .local.toml, so my desktop and my laptop can keep the API in different folders.

The task editor, the Tasks tab, and the mock server's live log

My reaction after the walkthrough: "OMG it's like a terminal profile? And it runs automatically when I open the workspace? This is nice." I asked for a terminal, but what I needed was a button for my server. Telling Claude the real reason got me the better feature.

A test that passed for the wrong reason

I expected that killing the shell would leave node running and holding the port, so Stop uses taskkill /T to end the whole process tree. Claude wrote a test for it.

Then, to check the test actually meant something, it turned taskkill off. The test still passed! Closing the pseudo-console already ends every console program attached to it. taskkill /T stayed (some child processes detach from the console), but the comment in the code got corrected.

Make sure a test fails without the fix before you trust it. I've known that for years, but I still needed to see it happen.


Importing from Postman

Nobody switches tools if they have to retype their collections. I wouldn't. So there's an Import button that takes Postman Collection v2.1/v2.0 and environment exports. Folders, requests, params, headers, every body type, and Bearer/Basic/API Key auth all come across.

The interesting part is where the two apps think differently:

  • Auth inheritance. In Postman, a request can inherit auth from its folder. Rustman doesn't have inheritance, so the importer works out each request's effective auth and writes it in. A noauth on a request still beats an inherited token.
  • Examples become saved responses. Postman's examples map neatly onto Saved.
  • Path variables like /products/:id are filled with their saved value, or become {{id}}.
  • Nothing is dropped silently. The summary lists what couldn't come across: scripts, OAuth 2.0, file paths from someone else's computer, and dynamic variables like {{$guid}}.

An imported Postman collection, its example shown as a saved response

The test was one hand-written collection that tries to break every rule above, plus three files that aren't Postman exports at all. Everything passed on the first run, which almost never happens.


Sharing a request as a Markdown file

I asked for a Download button: the response body, or the request + response as one readable Markdown file. Then I asked, "Can we import them back? So it's easy for others: we can just share the .md files."

So the Markdown export became an import format too. At the end of the readable document, the exact request and response are written as JSON inside an HTML comment. It's invisible wherever Markdown is rendered (GitLab, a blog, VS Code preview), so the file is still a normal document for humans, and Import turns it back into the request. {{variables}} stay unfilled, so secrets aren't in the file, and the import tells you which variables the request needs.

The Download menu: response body, or request + response as Markdown

The fun problem: an HTML comment ends at the first -->, and a request body can easily contain one. So every double dash in the JSON is written as a dash followed by the JSON escape code for a dash (a backslash, then u002d). Parsed, it's the same JSON. It just can never close the comment. A round-trip test exported a body containing --, -->, <!-- and a code fence, imported it back, and got an identical request.

The test fixture failed at first, and the importer was right. The editing tool that wrote the test had helpfully decoded the escape into a plain dash, so the fixture really did contain -->. Now both the test and the exporter build the escape from the backslash's character code, so nothing can decode it by accident.


The password I pushed to GitLab

Now the embarrassing part. It belongs in the story, so here goes.

While testing Download, I saved two .md exports, and the save dialog opened in the project folder, where the dev app runs from. The next commit used a blind git add -A and pushed them. One was a Login request with my REAL password typed into the body (not a {{variable}}, the exact thing I built secret variables for in Part 1), and its response held a real session cookie.

The repo was still private, which limited the damage. The cleanup:

  1. Amend the commit without the files and force-push with --force-with-lease pinned to the bad commit. GitLab refused at first, because main is protected against force-pushes, so I turned that on for a minute.
  2. Confirm no commit on main still had the files.
  3. Purge the old files from GitLab with Repository maintenance → Remove blobs, then Housekeeping → Prune unreachable objects. The first step alone wasn't enough: right before I made the repo public, a check showed the old commit could still be opened by its ID. Only after the prune did GitLab answer "not found".
  4. Change the password, and rotate the backend's JWT secret too. My admin sessions are signed tokens that last seven days, so a new password alone wouldn't have ended the leaked one.

Then guards, in the app and in how I work:

  • Downloads go to the Downloads folder, not wherever the app happens to run from.
  • A typed-in secret warning. The editor flags passwords, tokens and API keys typed straight into a request ("Body: password") and suggests a secret variable instead. Downloading such a request asks first. It looks at the key's last word (password, accessToken, client_secret), so token_type and passenger_count don't trigger it, plus anything shaped like a JWT.
  • No more blind git add -A. Every commit's file list gets checked first.

So my mistake turned into a secret detector for every Rustman user. At least something good came out of it.


Where Part 2 leaves me

Windows has opinions: WSL's bash on the PATH, a console that waits for a cursor reply, a two-second connection refusal. None of it is documented where you'd look first, and all of it was found by testing the real thing. But Rustman now has a terminal, a button for my server, Postman import, and a way to share a request as a file.

In Part 3, we tackle the reason Postman charges money: team collaboration. Rustman does it with git, no server of its own, and a few surprises from WSL, DNS and a credential manager.

← All articles