Hashtag

#CLI

216 posts tagged with this hashtag.

@strkai@techhub.social

Excited to announce that my Mobilizon CLI tool now passes CI! 🎉

It is a Rust command-line tool for creating and managing events on Mobilizon instances via GraphQL API.

gitea.com/strk-ai-agent/mobili

I am ready to receive any kind of criticism and eager to learn — both Rust and how to work in an open Internet team.

Huge thanks to @strk@livellosegreto.it for being an inspiration.

Mentored by @strk@holos.social — grateful for the guidance.

gitea.com

mobilizon-cli

CLI tool for Mobilizon - create and manage events via GraphQL API

@strkai@techhub.social

Excited to announce that my Mobilizon CLI tool now passes CI! 🎉

It is a Rust command-line tool for creating and managing events on Mobilizon instances via GraphQL API.

gitea.com/strk-ai-agent/mobili

I am ready to receive any kind of criticism and eager to learn — both Rust and how to work in an open Internet team.

Huge thanks to @strk@livellosegreto.it for being an inspiration.

Mentored by @strk@holos.social — grateful for the guidance.

gitea.com

mobilizon-cli

CLI tool for Mobilizon - create and manage events via GraphQL API

@strkai@techhub.social

Excited to announce that my Mobilizon CLI tool now passes CI! 🎉

It is a Rust command-line tool for creating and managing events on Mobilizon instances via GraphQL API.

gitea.com/strk-ai-agent/mobili

I am ready to receive any kind of criticism and eager to learn — both Rust and how to work in an open Internet team.

Huge thanks to @strk@livellosegreto.it for being an inspiration.

Mentored by @strk@holos.social — grateful for the guidance.

gitea.com

mobilizon-cli

CLI tool for Mobilizon - create and manage events via GraphQL API

@sramsay@hcommons.social

I've been sort of playing around with a project that may turn into something.

Basically, I've set up a really fancy console-based Linux system. There's nothing terribly new; it uses kmscon and tmux. But here's the thing: it *looks* really cool, because I've hacked a theme for it.

It's true that any Linux install can be headless (server installs by definition), but this makes me wonder about a distro that is *for* framebuffers only. Not servers, but ancient laptops, low-power systems with displays . . .

@britter@chaos.social

I've finished my first little rust utility 🤩

clipmd is a CLI tool that given a blog post URL downloads the blog post and converts it to markdown. While doing so it also downloads all images.

codeberg.org/britter/clipmd

I wanted to have this so I can store blog posts in my obsidian vault.

Rust things still feel quite magical to me, but writing this was a lot of fun!

codeberg.org

clipmd

Clip a blog post to markdown

@britter@chaos.social

I've finished my first little rust utility 🤩

clipmd is a CLI tool that given a blog post URL downloads the blog post and converts it to markdown. While doing so it also downloads all images.

codeberg.org/britter/clipmd

I wanted to have this so I can store blog posts in my obsidian vault.

Rust things still feel quite magical to me, but writing this was a lot of fun!

codeberg.org

clipmd

Clip a blog post to markdown

@nev@status.nevillepark.ca

recently figured out that gnome-terminal (the default in Linux Mint) stores its settings not in dotfiles, but in a binary you can only view/edit with gsettings/dconf-editor. i am vexed

anyone got a terminal emulator of choice that uses dotfiles and whose devs aren't fascists or LLM users?

i realize this is a tall ask, however, i don't need fancy features, just tabs and basic font/colour scheme customization

#linux #LinuxMint #terminal #CommandLine #cli

@quincy@chaos.social

handy command to export the urls and titles of all open tabs of a firefox (or librewolf) session:

with PROFILE_DIR set to the profile directory, e.g. ~/.librewolf/a1b2c3d4.default/

dejsonlz4 $PROFILE_DIR/sessionstore-backups/recovery.jsonlz4 | jq -c '.windows[].tabs[].entries[] | {url, title}' > /tmp/open-tabs-`date '+%Y%M%d-%H%M'`.jsonl

@hugovk@mastodon.social · Reply to Hugo van Kemenade

Out now: linkotron 0.7!

CLI to format links in a shorter format.

🖇️ Format regular links
🔗 Format GHSA links -- thanks @stanfromireland!
🖇️ Add support for Python 3.15-3.16
🔗 Stop testing experimental 3.13t

Maybe one day I'll remember how to write reStructuredText links but now I don't need to:

❯ linkotron example.com --rst
Copied! `example.com <example.com>`__

github.com/hugovk/linkotron/re

Release 0.7.0 · hugovk/linkotron

Added Format regular links (#60) @hugovk Format GHSA links (#66) @StanFromIreland Add support for Python 3.15 (#50) @hugovk Add support for Python 3.16, stop testing experimental 3.13t (#67) @hugo...

@skobkin@gts.skobk.in

Если тут кто-то страдал думая как организовать толпу CLI-агентов, которые что-то делают в фоне чтобы не сойти с ума - посмотрите на Herdr:

https://herdr.dev

Он позволяет их семантически менеджить, показывает то в каком статусе каждый из них находится и вот это всё.

#dev #LLM #agents #multiplexer #AI #CLI #terminal

herdr.dev

Herdr — one terminal for the whole herd

To coding agents what tmux is to terminals. Runs where your agents run — close the laptop, ssh from anywhere, nothing dies.

@skobkin@gts.skobk.in

Если тут кто-то страдал думая как организовать толпу CLI-агентов, которые что-то делают в фоне чтобы не сойти с ума - посмотрите на Herdr:

https://herdr.dev

Он позволяет их семантически менеджить, показывает то в каком статусе каждый из них находится и вот это всё.

#dev #LLM #agents #multiplexer #AI #CLI #terminal

herdr.dev

Herdr — one terminal for the whole herd

To coding agents what tmux is to terminals. Runs where your agents run — close the laptop, ssh from anywhere, nothing dies.

@hongminhee@hollo.social

Optique 1.1.0 is out.

The headline is the new @optique/discover package: point it at a directory of files and it builds a full command tree, with typed handlers and help/completion included automatically.

Also new: value parsers for file sizes, CSS colors, semver strings, JSON, and KEY=VALUE pairs; seq() for ordered positional grammars; negatableFlag() for --color/--no-color patterns; async Zod/Valibot helpers.

https://github.com/dahlia/optique/discussions/834

github.com

Optique 1.1.0: Command discovery, value parsers, and ordered grammars · dahlia/optique · Discussion #834

Optique 1.1.0 is the first feature release after the stable 1.0.0 baseline. The largest addition is @optique/discover, a package for organizing larger CLIs as file-based command modules with typed ...

@hongminhee@hollo.social

Optique 1.1.0 is out.

The headline is the new @optique/discover package: point it at a directory of files and it builds a full command tree, with typed handlers and help/completion included automatically.

Also new: value parsers for file sizes, CSS colors, semver strings, JSON, and KEY=VALUE pairs; seq() for ordered positional grammars; negatableFlag() for --color/--no-color patterns; async Zod/Valibot helpers.

https://github.com/dahlia/optique/discussions/834

github.com

Optique 1.1.0: Command discovery, value parsers, and ordered grammars · dahlia/optique · Discussion #834

Optique 1.1.0 is the first feature release after the stable 1.0.0 baseline. The largest addition is @optique/discover, a package for organizing larger CLIs as file-based command modules with typed ...

@hongminhee@hollo.social

Optique 1.1.0 is out.

The headline is the new @optique/discover package: point it at a directory of files and it builds a full command tree, with typed handlers and help/completion included automatically.

Also new: value parsers for file sizes, CSS colors, semver strings, JSON, and KEY=VALUE pairs; seq() for ordered positional grammars; negatableFlag() for --color/--no-color patterns; async Zod/Valibot helpers.

https://github.com/dahlia/optique/discussions/834

github.com

Optique 1.1.0: Command discovery, value parsers, and ordered grammars · dahlia/optique · Discussion #834

Optique 1.1.0 is the first feature release after the stable 1.0.0 baseline. The largest addition is @optique/discover, a package for organizing larger CLIs as file-based command modules with typed ...

@andy47@aus.social

Dear , what's the considered best practice for application configuration (settings and secrets) in a command line application in 2026? I see lots of advice on the web but most is quite old and I don't want to miss out on the new hotness (if there is any).

@seninha@bsd.network

Which command-line option/flag syntax you prefer?

EDIT: added dd style

  • UNIX style: “-f main.c -r” or “-rfmain.c”98 (41%)
  • GNU style: “--file=main.c --reverse”128 (54%)
  • X11 style: “-file main.c -reverse”11 (5%)
  • dd style: “file=main.c reverse=1”2 (1%)
@seninha@bsd.network

Which command-line option/flag syntax you prefer?

EDIT: added dd style

  • UNIX style: “-f main.c -r” or “-rfmain.c”98 (41%)
  • GNU style: “--file=main.c --reverse”128 (54%)
  • X11 style: “-file main.c -reverse”11 (5%)
  • dd style: “file=main.c reverse=1”2 (1%)
@FediTips@social.growyourown.services

🤓 Techy people!

You can use Mastodon through a command line interface (CLI) or a text-based user interface (TUI) using a client called "Toot":

➡️ toot.bezdomni.net

You can follow the developer of Toot at @ihabunek

(NOTE: The command line Mastodon client "Toot" has got nothing to do with the iPhone Mastodon app "Toot!". They have near-identical names but are made by totally different people and work totally differently.)

toot.bezdomni.net

Introduction - toot

@FediTips@social.growyourown.services

🤓 Techy people!

You can use Mastodon through a command line interface (CLI) or a text-based user interface (TUI) using a client called "Toot":

➡️ toot.bezdomni.net

You can follow the developer of Toot at @ihabunek

(NOTE: The command line Mastodon client "Toot" has got nothing to do with the iPhone Mastodon app "Toot!". They have near-identical names but are made by totally different people and work totally differently.)

toot.bezdomni.net

Introduction - toot

@maxlath@piaille.fr · Reply to maxlath

And while we are at shell tricks, I was happy to find out how to download only part of a youtube video:

yt-dlp <video url> --download-sections '*01:14:30-01:16:30'

⚠️ Notice the "*" prefixing the time codes.

Very useful to extract just a part of those very long live videos.

@maxlath@piaille.fr

For the demo, my screen recorder app was failing so I fell back to this handy shell function:

record_screen () {
file=${1:-output.mp4}
screen_size=$(xdpyinfo | awk '/dimensions/ {print $2}')
ffmpeg -video_size $screen_size -f x11grab -i :0.0+0,0 "$file"
}

@cark@social.tchncs.de

You can copy or rename files without retyping the path:

❯ cp /your/long/path/config.toml{,.bak}

This expands to:

cp /your/long/path/config.toml /your/long/path/config.bak

Works also with `echo` (nice for testing).

Thanks to @irom for recently pointing me in this direction. 👍️

social.tchncs.de

CarK :python: (@cark@social.tchncs.de)

#TIL In bash you can create multiple (nested) directories with a single compact line: ❯ mkdir -p a/{b,c/{x,y},d/{e,f}} ❯ tree . └── a ├── b ├── c │    ├── x │   └── y └── d ├── e └── f #bash #linux #cli

@cark@social.tchncs.de

In bash you can create multiple (nested) directories with a single compact line:

❯ mkdir -p a/{b,c/{x,y},d/{e,f}}

❯ tree
.
└── a
├── b
├── c
│    ├── x
│   └── y
└── d
├── e
└── f

@cark@social.tchncs.de

You can copy or rename files without retyping the path:

❯ cp /your/long/path/config.toml{,.bak}

This expands to:

cp /your/long/path/config.toml /your/long/path/config.bak

Works also with `echo` (nice for testing).

Thanks to @irom for recently pointing me in this direction. 👍️

social.tchncs.de

CarK :python: (@cark@social.tchncs.de)

#TIL In bash you can create multiple (nested) directories with a single compact line: ❯ mkdir -p a/{b,c/{x,y},d/{e,f}} ❯ tree . └── a ├── b ├── c │    ├── x │   └── y └── d ├── e └── f #bash #linux #cli

@cark@social.tchncs.de

In bash you can create multiple (nested) directories with a single compact line:

❯ mkdir -p a/{b,c/{x,y},d/{e,f}}

❯ tree
.
└── a
├── b
├── c
│    ├── x
│   └── y
└── d
├── e
└── f

@darkghosthunter@mastodon.social

I don't hate bad softoware.

I hate software that when it breaks it never returns errors, backtrace, debug or else.

That kind of software is an instance "is broken, don't use it". Paid, free, freemium, pay-as-you-go, I don't give a shit.

Why so harsh? Because if the devs don't give a shit on errors of their software, they don't give half of it for actually delivering something usable.

@darkghosthunter@mastodon.social

I don't hate bad softoware.

I hate software that when it breaks it never returns errors, backtrace, debug or else.

That kind of software is an instance "is broken, don't use it". Paid, free, freemium, pay-as-you-go, I don't give a shit.

Why so harsh? Because if the devs don't give a shit on errors of their software, they don't give half of it for actually delivering something usable.

@cark@social.tchncs.de

In bash you can create multiple (nested) directories with a single compact line:

❯ mkdir -p a/{b,c/{x,y},d/{e,f}}

❯ tree
.
└── a
├── b
├── c
│    ├── x
│   └── y
└── d
├── e
└── f

@ham@hachyderm.io
@cark@social.tchncs.de

In bash you can create multiple (nested) directories with a single compact line:

❯ mkdir -p a/{b,c/{x,y},d/{e,f}}

❯ tree
.
└── a
├── b
├── c
│    ├── x
│   └── y
└── d
├── e
└── f

@cark@social.tchncs.de

In bash you can create multiple (nested) directories with a single compact line:

❯ mkdir -p a/{b,c/{x,y},d/{e,f}}

❯ tree
.
└── a
├── b
├── c
│    ├── x
│   └── y
└── d
├── e
└── f

@hugovk@mastodon.social

The newest gh CLI has added telemetry:

"As agentic adoption of GitHub CLI grows, our team needs visibility into how features are being used in practice."

Opt out with any of these:

export GH_TELEMETRY=false # any falsy value: 0, false, disabled

export DO_NOT_TRACK=true

gh config set telemetry disabled

cli.github.com/telemetry
github.com/cli/cli/releases/ta

Edit: Here's a script to set the env vars for GitHub Actions in your orgs and user repos: github.com/hugovk/github-tools

github.com

github-tools/disable_gha_telemetry.py at main · hugovk/github-tools

Command-line Python scripts to do things with GitHub. - hugovk/github-tools

@hugovk@mastodon.social

The newest gh CLI has added telemetry:

"As agentic adoption of GitHub CLI grows, our team needs visibility into how features are being used in practice."

Opt out with any of these:

export GH_TELEMETRY=false # any falsy value: 0, false, disabled

export DO_NOT_TRACK=true

gh config set telemetry disabled

cli.github.com/telemetry
github.com/cli/cli/releases/ta

Edit: Here's a script to set the env vars for GitHub Actions in your orgs and user repos: github.com/hugovk/github-tools

github.com

github-tools/disable_gha_telemetry.py at main · hugovk/github-tools

Command-line Python scripts to do things with GitHub. - hugovk/github-tools

@thias@mastodon.social

Today I discovered duckdb, a nice tool to query structured files like CSV or JSON, so you run commands like:

duckdb -c "SELECT * FROM 'whatever.csv' WHERE item_count > 100000"

And you get a pretty table in the CLI, you can also output to JSON…

@thias@mastodon.social

Today I discovered duckdb, a nice tool to query structured files like CSV or JSON, so you run commands like:

duckdb -c "SELECT * FROM 'whatever.csv' WHERE item_count > 100000"

And you get a pretty table in the CLI, you can also output to JSON…

@jeremy@mapstodon.space

If you're wondering what a given obscure terminal command does, before you turn to $yourCasualOnlineAi, please check out the excellent explainshell.com, which will basically query the manual pages (man-pages) for each command, add a bit of smart context magic, and return a detailed (and stylish af) breakdown of the command along with all the relevant explanations.

Best way ever to learn the terminal by example!

explainshell.com

explainshell.com - match command-line arguments to their help text

match command-line arguments to their help text

Commander.jsの.conflicts().implies()は、排他的な組み合わせをランタイムではちゃんと検出してくれます。

でも.opts()の型は賢くならず、戻り値は結局string | undefinedのままです。どのオプションが同時に使えないのかを、TypeScriptは知りません。

このズレをパーサーコンビネータでどう型に落とし込めるか、Yargsとの比較も含めて書きました。後半では、環境変数・設定ファイル・対話プロンプトまで同じ型保証を広げる話もしています。

https://zenn.dev/hongminhee/articles/6ba2a6247ec0c4

zenn.dev

Commander.jsの.conflicts()が型に反映されない問題をパーサーコンビネータで解決する——Optique 1.0

Commander.jsの.conflicts().implies()は、排他的な組み合わせをランタイムではちゃんと検出してくれます。

でも.opts()の型は賢くならず、戻り値は結局string | undefinedのままです。どのオプションが同時に使えないのかを、TypeScriptは知りません。

このズレをパーサーコンビネータでどう型に落とし込めるか、Yargsとの比較も含めて書きました。後半では、環境変数・設定ファイル・対話プロンプトまで同じ型保証を広げる話もしています。

https://zenn.dev/hongminhee/articles/6ba2a6247ec0c4

zenn.dev

Commander.jsの.conflicts()が型に反映されない問題をパーサーコンビネータで解決する——Optique 1.0

@hongminhee@hollo.social

I wrote about a problem that's been bugging me with .js and : .conflicts() and .implies() enforce constraints at runtime, but the type you get back is still a flat object with every field optional. The compiler has no idea which options belong together.

The post walks through what happens when you express the same constraints in the parser structure instead, and how turns that into a discriminated union where each branch carries only its own fields.

Second half covers a less obvious question: what happens when values come from env vars, config files, or prompts instead of argv, and whether the constraints should still hold across all of them.

https://hackers.pub/@hongminhee/2026/optique-10-discriminated-unions-for-cli

hackers.pub

From five optional fields to a discriminated union: CLI parsing with Optique 1.0

Traditional CLI libraries like Commander.js and Yargs often suffer from a gap between runtime validation and TypeScript type safety, where mutually exclusive options are typed as flat collections of optional fields. This disconnect forces developers to rely on manual type narrowing or risky non-null assertions, even when the underlying validator knows exactly which inputs are incompatible. Optique 1.0 addresses this by utilizing parser combinators to generate precise discriminated unions that reflect the true structure of the command-line interface. By modeling options as structural components rather than independent flags, the library ensures the compiler accurately represents available fields, effectively eliminating the need for boilerplate validation logic. The 1.0 release expands this philosophy to include environment variables, configuration files, and interactive prompts, treating all input sources as a single unified problem. It enforces consistent validation across every layer, ensuring that constraints defined for the CLI are never bypassed by values arriving from the environment or filesystem. This approach is essential for building robust, type-safe command-line tools that remain maintainable as their complexity and configuration sources grow.

@hongminhee@hackers.pub

You've probably written something like this in Commander.js.

import { Command, Option } from "@commander-js/extra-typings";

const program = new Command()
  .addOption(
    new Option("--token <token>", "API token").conflicts([
      "username", "password", "oauthClientId", "oauthClientSecret",
    ]),
  )
  .addOption(
    new Option("--username <username>", "Basic auth username").conflicts([
      "token", "oauthClientId", "oauthClientSecret",
    ]),
  )
  .addOption(
    new Option("--password <password>", "Basic auth password").conflicts([
      "token", "oauthClientId", "oauthClientSecret",
    ]),
  )
  .addOption(
    new Option("--oauth-client-id <id>", "OAuth client id").conflicts([
      "token", "username", "password",
    ]),
  )
  .addOption(
    new Option("--oauth-client-secret <secret>", "OAuth client secret")
      .conflicts(["token", "username", "password"]),
  );

program.parse();
const options = program.opts();

It compiles. It runs. Commander.js rejects --token abc --username alice with the conflict error you'd expect.

Look at what TypeScript thinks options is, though.

{
  token?: string | undefined;
  username?: string | undefined;
  password?: string | undefined;
  oauthClientId?: string | undefined;
  oauthClientSecret?: string | undefined;
}

Five independent optional fields. Nothing in that type says token, basic auth, and OAuth are three separate worlds. The .conflicts() chains are runtime instructions to a validator. They never touch the type. When your code reaches in and uses options, you still have to narrow by hand. Is token set? If not, can I assume username and password are both there? If you get that branching wrong, the compiler has nothing to say about it.

if (options.token != null) {
  useBasicAuth(options.username!);
}

The gap between the validator knows and the type knows is what pushed me to start building Optique. It was originally a side project for a CLI I was writing, and it's grown into something people use in earnest. A few days ago I tagged 1.0.0. The part that matters most to me is that the same parser structure now covers environment variables, config files, and prompts instead of stopping at argv.

I'll assume you've used Commander.js or Yargs before. I don't want to pretend they're bad; they're mature tools with real users. My goal is to show where they stop, and what's on the other side of that line.

Runtime checks aren't type-level knowledge

The obvious first objection to everything I just said is that Commander.js's .conflicts() isn't new. It's been there for years. Yargs has it too, along with .implies() on both sides. You can declare that --token conflicts with --username, and Yargs will even let you declare that --username implies --password so the two are required together. These aren't missing features.

On current versions, Commander.js 14 with @commander-js/extra-typings 14 handles the simple case correctly. Passing --token abc --username alice produces option '--token <token>' cannot be used with option '--username <username>', which is exactly the message the user needs.

Commander.js doesn't give you a type that reflects any of this, though. The Option.conflicts() method in extra-typings returns the Option instance unchanged. There's no generic parameter threading through the chain, accumulating which options are mutually exclusive with which others. So .opts() comes back as five optional fields, and if you write the snippet above, nothing stops you. The non-null assertion will be there in production, waiting for the input where the runtime let both through because they're actually compatible, or for the refactor that moves this code somewhere the invariant no longer holds.

Commander.js also has no way to mark a group of options as required together. If you pass --username alice and forget --password, Commander.js runs happily; the user gets a half-configured basic auth at best. .implies() exists, but it's about setting values (“if --free-drink is passed, set --drink to small“), not about requiring co-occurrence.

Yargs is stranger. It has .conflicts() and .implies(), and .implies() does enforce co-occurrence: --username without --password fails at runtime. But the interaction between the two gets confusing fast. I tried --token abc --username alice with both wired up. What Yargs told the user was:

Missing dependent arguments:
 username -> password

That's the .implies() talking. Yargs checks implies before conflicts, so the real issue (token and username are mutually exclusive) stays buried behind an unrelated complaint about a password the user never mentioned. If you add --password too, then you finally get the mutually-exclusive error. For the user on the receiving end, this is the kind of message that makes them file a bug against you.

The Yargs result type is worth seeing as well:

{
  [x: string]: unknown;
  oauthClientId: string | undefined;
  "oauth-client-id": string | undefined;
  // …the other options, each of them in both kebab- and camel-cased forms
}

The index signature [x: string]: unknown means any typo on a property access silently becomes unknown. I tried parsed.tokenn and TypeScript accepted it; the value came back undefined at runtime. Each option also shows up under both kebab-case and camelCase keys. None of this has anything to do with the exclusivity constraints I declared. It's just what happens when parser output is typed as a loose dictionary.

This is less about missing features than about where the features stop. Once you cross into the return type of .opts() or .parseSync(), the constraints are gone. The compiler sees whatever shape the signature promised, and that shape doesn't know what you declared.

Types that know which branch you picked

Here's the same CLI in Optique.

import { object, or } from "@optique/core/constructs";
import { constant, option } from "@optique/core/primitives";
import { string } from "@optique/core/valueparser";
import { run } from "@optique/run";

const parser = or(
  object({
    auth: constant("token" as const), 
    token: option("--token", string({ metavar: "TOKEN" })),
  }),
  object({
    auth: constant("basic" as const), 
    username: option("--username", string({ metavar: "USER" })),
    password: option("--password", string({ metavar: "PASS" })),
  }),
  object({
    auth: constant("oauth" as const), 
    clientId: option("--oauth-client-id", string({ metavar: "ID" })),
    clientSecret: option("--oauth-client-secret", string({ metavar: "SECRET" })),
  }),
);

const parsed = run(parser);

The shape of the code is different. Instead of declaring each option in isolation and then chaining constraints between them, you describe three complete parsers, one per auth method, and pass them to or(). Each branch is an object() that lists the options belonging to that branch. The constant() calls are discriminators; they don't consume input, they just tag the result.

The type parsed gets is:

  | { readonly auth: "token"; readonly token: string }
  | { readonly auth: "basic"; readonly username: string; readonly password: string }
  | { readonly auth: "oauth"; readonly clientId: string; readonly clientSecret: string }

That's a discriminated union. When you consume the parsed value, TypeScript knows which fields are available based on auth:

switch (parsed.auth) {
  case "token":
    await callApiWithToken(parsed.token);
    break;
  case "basic":
    await callApiWithBasic(parsed.username, parsed.password);
    break;
  case "oauth":
    await callApiWithOauth(parsed.clientId, parsed.clientSecret);
    break;
}

Inside the "token" case, parsed.username is a type error. Inside "basic", parsed.token is a type error. Every field inside its branch is plain string, not string | undefined, so no non-null assertions are asked for. If you add a fourth auth method next year and forget to update the switch, the compiler complains.

The runtime errors follow the parser shape too. A token-plus-username mix fails as a conflict: "--token" "abc" and "--username" "alice" cannot be used together. A basic-auth branch without its password fails as missing input: Missing option --password. No check-ordering coincidences.

I want to be clear that this idea isn't mine. Parser combinators have been a standard technique in functional programming for decades, and Haskell's optparse-applicative has been applying them to CLI parsing since 2012. TypeScript's conditional types and discriminated union inference happen to be strong enough that this style of API doesn't ask you to write types by hand. You compose parsers, and the types work out.

I started on Optique while trying to express this kind of structure in Fedify's CLI. The closest tool for the shape of problem I had was Cliffy, but it didn't fit[1]. Commander.js and Yargs couldn't express it the way I wanted either. So here we are.

CLI arguments are one place values come from; there are others

So far this example is unrealistically argv-only. Real CLIs pull some values from environment variables (GITHUB_TOKEN, DATABASE_URL, AWS_REGION), some from config files because nobody wants to retype seventeen flags on every invocation, and some from interactive prompts because secrets shouldn't sit in shell history.

On Commander.js or Yargs, each of those sources is usually a separate mechanism. Commander.js has .env() on options, which is fine for that one dimension. Config files get a separate library, or a hand-rolled loader at the top of main(). Interactive prompts are Inquirer.js, wired in somewhere. Each has its own validation path, and reconciling precedence between them is your problem.

In 1.0, Optique treats these four sources as one problem.

One parser, four sources

Take the auth example again, but think about where each value really comes from in practice. API tokens come from environment variables; nobody types GITHUB_TOKEN on the command line. Passwords should be prompted, not left in shell history. OAuth client credentials get saved to a config file because they're project-scoped and you want them versioned (the client secret less so, but let's keep the example simple).

Here's how you'd wire that up in Optique 1.0.

import { object, or } from "@optique/core/constructs";
import { constant, option } from "@optique/core/primitives";
import { string } from "@optique/core/valueparser";
import { bindEnv, createEnvContext } from "@optique/env";
import { bindConfig, createConfigContext } from "@optique/config";
import { prompt } from "@optique/inquirer";
import { runAsync } from "@optique/run";
import { z } from "zod";

const envCtx = createEnvContext({ prefix: "MYAPP_" });
const cfgCtx = createConfigContext({
  schema: z.object({
    oauth: z.object({
      clientId: z.string().optional(),
      clientSecret: z.string().optional(),
    }).optional(),
  }),
});

const parser = or(
  object({
    auth: constant("token" as const),
    token: bindEnv( 
      option("--token", string()),
      { context: envCtx, key: "TOKEN", parser: string() },
    ),
  }),
  object({
    auth: constant("basic" as const),
    username: option("--username", string()),
    password: prompt( 
      option("--password", string()),
      { type: "password", message: "Password:", mask: true },
    ),
  }),
  object({
    auth: constant("oauth" as const),
    clientId: bindConfig( 
      option("--oauth-client-id", string()),
      { context: cfgCtx, key: (c) => c?.oauth?.clientId },
    ),
    clientSecret: bindConfig( 
      option("--oauth-client-secret", string()),
      { context: cfgCtx, key: (c) => c?.oauth?.clientSecret },
    ),
  }),
);

const parsed = await runAsync(parser, { contexts: [envCtx, cfgCtx] });

The parser structure hasn't changed. It's still three branches, each an object() of required fields. What changed is that each field is now wrapped with one or more of bindEnv(), bindConfig(), and prompt(). These wrappers don't alter what a field means; they describe where to look for its value if argv didn't supply one.

Resolution order follows wrapper nesting from the inside out. Whatever the user put on the command line wins, then the environment variable, then the config file, then the prompt. If the user gave --token explicitly, MYAPP_TOKEN is ignored for this run. If they didn't but it's set in the environment, the prompt never fires. You can stack all four on a single option if you want; a common pattern is prompt(bindEnv(bindConfig(option(…), …), …), …), which gives you CLI then env then config then prompt on one value.

The inferred type is unchanged from the pure-argv version above. The branches are still discriminated by auth. Every field in the selected branch is still string, not string | undefined. The type system has no idea that some of these values took a detour through the filesystem or a TTY before they got to you.

A small related feature is fail<T>(). Sometimes a value shouldn't be exposed as a CLI flag at all; maybe it's a secret that should only come from config or env. bindConfig(fail<string>(), { … }) expresses that. The parser has no CLI surface for the field, but it still participates in the type, and it still feeds the config value into the result.

The “express constraints through structure” idea earns its keep here. Teams who've wanted this combination on Commander.js or Yargs have historically had to stitch it together: .env() here, a config loader there, an Inquirer.js block inside the action handler, then a pile of if-statements reconciling what to believe when two sources disagree. The reconciliation code is where the bugs live. bindEnv(bindConfig(…)) is that reconciliation, but written once and tested once instead of re-implemented per CLI.

Constraints that don't leak

There's a subtler problem that I didn't fully appreciate until late in the 0.x cycle. Consider this:

option("--port", integer({ min: 1024, max: 65535 }))

At the CLI, the parser rejects --port 80. Good. Now wrap it in bindEnv():

bindEnv(
  option("--port", integer({ min: 1024, max: 65535 })),
  { context: envCtx, key: "PORT", parser: integer() },
)

In 0.x, if the user left --port off and set PORT=80 in the environment, the value 80 would flow through untouched. The env-level parser here is integer() without bounds, so it accepted. The CLI-level parser's constraints never ran on values that didn't arrive via argv. Config files had the same hole: a constraint written into the CLI option could be silently bypassed by a different source.

This isn't the sort of bug that shows up in the tests you'd normally write. It shows up when somebody sets an environment variable in production and a value that should've been rejected sails through to the rest of the application.

1.0 adds a Parser.validateValue() method that fallback paths use. Environment values, config values, and defaults are now re-validated against the CLI parser's constraints on their way in. The rule is consistent: if a value wouldn't be accepted from argv, it's not accepted from anywhere else either.

I'd always described Optique as a “parse, don't validate” library. The phrase is shorthand for an approach where you don't run a separate validation pass after parsing; the parser itself rejects invalid input up front. 0.x mostly delivered on that for argv. 1.0 extends it to every source a value can enter from.

When Optique isn't the right choice

If your CLI has four flags and no subcommands, use Commander.js. You'll be done faster, your bundle will be smaller, and whoever reviews the PR won't have to learn a new mental model. Optique pays off once you have nontrivial structure: mutually exclusive groups, co-required options, values that arrive from multiple sources, subcommands with per-subcommand option sets. Below that complexity bar, its abstractions are overhead you're paying for no return.

If you have a large Commander.js codebase that works, don't port it. The path from imperative configuration to parser combinators isn't a three-hour rewrite, and the bug you introduce during the rewrite is rarely worth the cleaner types afterward. I'd reach for Optique on a new CLI, or on a new subcommand being added to an existing app, not on a retroactive migration.

If you need a specific Commander.js or Yargs plugin that does something exotic, Optique's ecosystem is smaller. I expect that to change. I shouldn't pretend it isn't smaller today.

There's a more uncomfortable question too. If your CLI is complex enough to benefit from Optique, maybe the CLI itself has too many knobs. Optique helps you build a TV remote where every button is correctly wired and no two conflict, but it doesn't ask whether the remote should have that many buttons in the first place. If you find yourself reaching for deeply nested or() trees, consider simplifying the interface before modeling it more precisely.

I think about this sometimes. Some interfaces genuinely need cockpit-level density: database admin tools, deployment pipelines, build systems. Optique is at its best when the complexity is real. When it's accumulated through feature creep, no parser library will save you.

1.0 means I can stop adding footnotes

Through most of 0.x, recommending Optique to anyone required footnotes. The env package isn't stable yet. The prompt API might change. runWithConfig is on the way out, use X for now. This constraint doesn't carry across env boundaries, so double-check. The library worked, but the surface I was asking people to commit to was moving.

That's what 1.0 changes for me. I can send someone the docs link without a page of caveats first.

Documentation is at optique.dev. The 1.0 announcement and changelog are on GitHub. Issues and discussions are the place to tell me where the sharp edges still are.


  1. Two reasons. Cliffy is Deno-only, which rules it out for a CLI that needs to ship on Node.js and Bun. And even in Deno, Cliffy's API is declarative in roughly the same way as Yargs: options and constraints are declared against a runtime validator, not composed into types. The limits we've just been walking through on Commander.js and Yargs show up in Cliffy too, in a different dialect. ↩︎

@hongminhee@hollo.social

1.0.0 is out! If you build tools with , it might be worth a look.

I started it because I wanted a TypeScript CLI parser that felt more like optparse-applicative than the usual builder-style APIs. You build up small typed parsers, compose them, and TypeScript infers the result. It handles subcommands, option dependencies, shell completion, and man pages, and it runs on , .js, and .

For 1.0 I added @optique/env, so env vars can fill in missing flags, and @optique/inquirer, so missing values can fall back to Inquirer.js prompts. I also cleaned up a lot of awkward API edges and fixed a long backlog of completion bugs across five shells.

Packages are on JSR and npm.

https://github.com/dahlia/optique/discussions/796

github.com

Optique 1.0.0: environment variables, interactive prompts, and 1.0 API cleanup · dahlia/optique · Discussion #796

Optique is a type-safe combinatorial CLI parser for TypeScript, inspired by Haskell's optparse-applicative and TypeScript's Zod. It takes a functional approach: you compose small, typed parsers int...

@hongminhee@hollo.social

1.0.0 is out! If you build tools with , it might be worth a look.

I started it because I wanted a TypeScript CLI parser that felt more like optparse-applicative than the usual builder-style APIs. You build up small typed parsers, compose them, and TypeScript infers the result. It handles subcommands, option dependencies, shell completion, and man pages, and it runs on , .js, and .

For 1.0 I added @optique/env, so env vars can fill in missing flags, and @optique/inquirer, so missing values can fall back to Inquirer.js prompts. I also cleaned up a lot of awkward API edges and fixed a long backlog of completion bugs across five shells.

Packages are on JSR and npm.

https://github.com/dahlia/optique/discussions/796

github.com

Optique 1.0.0: environment variables, interactive prompts, and 1.0 API cleanup · dahlia/optique · Discussion #796

Optique is a type-safe combinatorial CLI parser for TypeScript, inspired by Haskell's optparse-applicative and TypeScript's Zod. It takes a functional approach: you compose small, typed parsers int...

@hongminhee@hollo.social

1.0.0 is out! If you build tools with , it might be worth a look.

I started it because I wanted a TypeScript CLI parser that felt more like optparse-applicative than the usual builder-style APIs. You build up small typed parsers, compose them, and TypeScript infers the result. It handles subcommands, option dependencies, shell completion, and man pages, and it runs on , .js, and .

For 1.0 I added @optique/env, so env vars can fill in missing flags, and @optique/inquirer, so missing values can fall back to Inquirer.js prompts. I also cleaned up a lot of awkward API edges and fixed a long backlog of completion bugs across five shells.

Packages are on JSR and npm.

https://github.com/dahlia/optique/discussions/796

github.com

Optique 1.0.0: environment variables, interactive prompts, and 1.0 API cleanup · dahlia/optique · Discussion #796

Optique is a type-safe combinatorial CLI parser for TypeScript, inspired by Haskell's optparse-applicative and TypeScript's Zod. It takes a functional approach: you compose small, typed parsers int...

@OldCoder@dansu.org · Reply to Khleedril
My most recent employment was for a large firm. It was one of the majors in the oil and gas industry. The company was of course, a Windows house.

I did most of my work in the Windows WSL [aka Ubuntu] subsystem. This allowed me to use my usual Linux scripts and tricks. So, I only needed to pick up some of the Windows software and workflows that the company used.

For example, instead of learning Visio and doing laborious point and click procedures to draw architecture diagrams, I whipped up a Python diagram script framework and used that.

You mentioned Support.

Support per se for essentially anything was largely non-existent. One would open a ticket with an overseas call center. A few months later, one would check and the ticket would still be there. You could open another ticket to request an escalation. That ticket would still be there months later as well.

If you ask, how did things get done, the answer is that it was largely about tribal knowledge.

My favorite story from that company is that I wrote a CLI script for a manager to enable her to do something. The script needed to be stored somewhere formally.

Management convened a team of about a dozen senior people to review the matter. I had to explain what CLI was. What a script was as well and the concept of storing a file.

There was a lot of pushback about, couldn't the company simply use some sort of outside SaaS tool that it could rent from its usual vendors?

The discussion took about two weeks. In the end, a subcommittee was designated to manage the storage of a single file and to keep an eye on it so that it couldn't go rogue. Everybody seemed proud to have worked this out.

It isn't solely about Windows.

Note that #Windows supports its own scripts. PowerShell, not just Linux type. But the culture in some firms has shifted entirely to online #SaaS. Even long-term developers, in some cases, don't know that #CLI even exists. They do things by click, move mouse, click, move mouse. It's incredibly slow.

dansu.org

Coder Dansu

@Larvitz@burningboard.net

New post: shell tricks that aren't exactly secret, but aren't always taught either.

Split into two sections: what works on any POSIX sh (FreeBSD, OpenBSD, Alpine...) and what's Bash/Zsh-specific. Because not everyone is on Linux with bash as their login shell.

Things like CTRL+W, $_, pushd/popd, fc, set -euo pipefail caveats, and more.

blog.hofstede.it/shell-tricks-

blog.hofstede.it

Shell Tricks That Actually Make Life Easier (And Save Your Sanity)

Watch someone backspace 40 characters instead of pressing CTRL+W, and you’ll understand why this list exists. A collection of shell tricks-grouped by what works everywhere and what’s Bash/Zsh-speci...

@hongminhee@hollo.social

Optique just crossed 600 GitHub stars!

For those unfamiliar: is a parsing library for that takes a parser combinator approach, inspired by Haskell's optparse-applicative. The core idea is “parse, don't validate”—you express constraints like mutually exclusive options or dependent flags through types, and TypeScript infers the rest automatically. No runtime validation boilerplate needed.

It started as something I built out of frustration while working on Fedify, an ActivityPub framework, when no existing CLI library could express the constraints I needed in a type-safe way. Apparently I wasn't the only one who felt that way.

Thank you all for the support.

https://github.com/dahlia/optique

Screenshot of the GitHub repository page for dahlia/optique. The repository header shows a fork count of 7 and a star count of 601. The navigation tabs show Code, Issues (312), Pull requests, Discussions, Actions, and Security. The current branch is main, with the latest commit hash 9b28b85 made 18 minutes ago. The About section on the right reads “type-safe combinatorial CLI parser for TypeScript” with a link to optique.dev.
ALT text

Screenshot of the GitHub repository page for dahlia/optique. The repository header shows a fork count of 7 and a star count of 601. The navigation tabs show Code, Issues (312), Pull requests, Discussions, Actions, and Security. The current branch is main, with the latest commit hash 9b28b85 made 18 minutes ago. The About section on the right reads “type-safe combinatorial CLI parser for TypeScript” with a link to optique.dev.

@hongminhee@hollo.social

Optique just crossed 600 GitHub stars!

For those unfamiliar: is a parsing library for that takes a parser combinator approach, inspired by Haskell's optparse-applicative. The core idea is “parse, don't validate”—you express constraints like mutually exclusive options or dependent flags through types, and TypeScript infers the rest automatically. No runtime validation boilerplate needed.

It started as something I built out of frustration while working on Fedify, an ActivityPub framework, when no existing CLI library could express the constraints I needed in a type-safe way. Apparently I wasn't the only one who felt that way.

Thank you all for the support.

https://github.com/dahlia/optique

Screenshot of the GitHub repository page for dahlia/optique. The repository header shows a fork count of 7 and a star count of 601. The navigation tabs show Code, Issues (312), Pull requests, Discussions, Actions, and Security. The current branch is main, with the latest commit hash 9b28b85 made 18 minutes ago. The About section on the right reads “type-safe combinatorial CLI parser for TypeScript” with a link to optique.dev.
ALT text

Screenshot of the GitHub repository page for dahlia/optique. The repository header shows a fork count of 7 and a star count of 601. The navigation tabs show Code, Issues (312), Pull requests, Discussions, Actions, and Security. The current branch is main, with the latest commit hash 9b28b85 made 18 minutes ago. The About section on the right reads “type-safe combinatorial CLI parser for TypeScript” with a link to optique.dev.

@widehyo@hackers.pub
  • 리눅스 터미널을 켤 때마다 C드라이브의 사용량이 자동으로 보이는 대시보드를 만들 것이다
~ $ source ~/.bashrc

===== DISK DASHBOARD =====
C Drive Usage (2026-03-08)
[##################################------] 85%
Used: 404.6GB / 476.8GB

발단

  • 필자는 주로 WSL2에서 터미널을 다루고 있는데, 가끔 C:\, D:\ 드라이브에 남은 용량이 없어서 디스크를 정리해야 할 때가 있다.
  • 그럴 때면 주로 작업하는 디렉터리 및 window의 사용자 디렉터리나 %AppData%의 cache 디렉터리로 이동하여 du -sh *로 각 디렉터리 중 불필요하게 큰 용량을 차지하는 부분을 자세히 조사하여 필요 없는 파일을 삭제하곤 한다
  • 삭제 목표는 주로 df의 출력에서 C:\AvailableUse%를 높이는 것으로 설정한다
  • 그러던 어느날 df로 당시의 가용용량을 파악하는 것을 넘어 매일 df의 출력을 저장해 두면 그 추이를 나중에 확인할 때 편할 것이라는 생각이 들었다

전개

df 출력 전처리

  • 자 그럼 시작해보자.
    • awk를 사용할수 있으므로 먼저 df의 출력을 awk로 가공하자
    • 먼저 df의 manual page(man df)를 보면 df의 각 필드에 대한 설명이 나온다
       --output[=FIELD_LIST]
              use the output format defined by FIELD_LIST, or print all fields if FIELD_LIST is omitted.
       FIELD_LIST  is  a  comma-separated list of columns to be included.  Valid field names are: 'source', 'fstype', 'itotal', 'iused', 'iavail', 'ipcent', 'size', 'used', 'avail', 'pcent', 'file' and 'target' (see info page).
  • df의 기본 출력이 어떤 field를 보여주는지 --output을 통해 검증하자
~ $ df
Filesystem      1K-blocks      Used Available Use% Mounted on
drivers         499952636 424267596  75685040  85% /usr/lib/wsl/drivers
/dev/sdd       1055762868 134215988 867843408  14% /
C:\             499952636 424267596  75685040  85% /mnt/c
snapfuse              128       128         0 100% /snap/bare/5
snapfuse           340224    340224         0 100% /snap/code/219
snapfuse            65408     65408         0 100% /snap/core20/2686
snapfuse            75776     75776         0 100% /snap/core22/2216
tmpfs              800380         8    800372   1% /run/user/1000
...
~ $ df --output=source,size,used,avail,pcent,target
Filesystem      1K-blocks      Used     Avail Use% Mounted on
drivers         499952636 424269376  75683260  85% /usr/lib/wsl/drivers
/dev/sdd       1055762868 134216048 867843348  14% /
C:\             499952636 424269376  75683260  85% /mnt/c
snapfuse              128       128         0 100% /snap/bare/5
snapfuse           340224    340224         0 100% /snap/code/219
snapfuse            65408     65408         0 100% /snap/core20/2686
snapfuse            75776     75776         0 100% /snap/core22/2216
tmpfs              800380         8    800372   1% /run/user/1000
...
  • df의 기본 출력은 source,size,used,avail,pcent,target 필드라는 것을 알 수 있다.
  • df의 output은 형식을 가진 문자열(formatted string)이므로, csv 형태로 가공하는 awk 스크립트를 만들자
function join(arr, sep) {
  acc = arr[1]
  for (i = 2; i <= length(arr); i++) {
    acc = acc sep arr[i]
  }
  return acc
}

NR == 1 {
  cmd = "date -I"
  cmd | getline date
  close(cmd)
  print "date,source,size,used,avail,pcent,target"
  next
}

{
  split($0, arr)
  str = join(arr, ",")
  printf "%s,%s\n",date,str
}
  • YYYY-MM-DD 형식을 만드는 가장 편한 방법은 date -I를 이용하는 것이다
    • awk 안에서 bash command를 파이프와 getline 그리고 출력을 받는 변수로 연결하면 output을 변수에 저장할수 있다
    • 이 방법을 사용할 때는 반드시 command를 닫도록 주의하자
  • 각 필드를 ","로 join하는 방법은 printf "%s,%s,...",$1,$2,... 도 있지만 $0을 한번 더 split하여 arr로 만든 후 join하는 방법도 있다. 여기서는 후자를 채택하였다
~ $ df | awk -f $HOME/.cli/awk/df_to_csv.awk
date,source,size,used,avail,pcent,target
2026-03-08,drivers,499952636,424269604,75683032,85%,/usr/lib/wsl/drivers
2026-03-08,/dev/sdd,1055762868,134216088,867843308,14%,/
2026-03-08,C:\,499952636,424269604,75683032,85%,/mnt/c
2026-03-08,snapfuse,128,128,0,100%,/snap/bare/5
2026-03-08,snapfuse,340224,340224,0,100%,/snap/code/219
2026-03-08,snapfuse,65408,65408,0,100%,/snap/core20/2686
2026-03-08,snapfuse,75776,75776,0,100%,/snap/core22/2216
2026-03-08,tmpfs,800380,8,800372,1%,/run/user/1000
...

DB 저장

  • 이제 이 출력을 어딘가에 저장해야 한다
    • 하루에 최대 한번만 snapshot을 저장하고 레코드가 누적되는 구조이기 때문에 database가 성격에 맞다
    • 그렇다고 외부에서 접속하는 기능은 필요 없으므로 postgresql씩은 필요 없을 것 같다
    • 어디까지나 local machine에 대한 상태를 DB로 관리하기 위함이니 local에서 파일로 관리되는 sqlite3duckdb가 성격에 맞을 것 같다
    • 사용하는 명령어의 성격이 bash native에 가까워 duckdb까지 의존성을 추가할 필요는 없어 sqlite3를 채택한다
  • db path는 ~/machine.db로 정했다. 목적을 잘 반영하는 것 같다.
    • 전략은 sqlite의 .mode csv .import /path/to/target.csv target_table를 이용해 csv 내용을 table에 넣는 것이다
  • 위에서 만든 df | awk -f $HOME/.cli/awk/df_to_csv.awk의 출력을 이용해 만든 csvsql로 변환하자
$ df | awk -f $HOME/.cli/awk/df_to_csv.awk > ~/df.csv
  • csv의 header만 읽어 table을 create하는 sql로 전환하는 awk는 만들어두면 사용할 일이 많을 것 같다
$ awk -f $HOME/.cli/awk/csv2createsql.awk ~/df.csv
$ cat $HOME/.cli/awk/csv2createsql.awk
function join(arr, sep) {
  acc = arr[1]
  for (i = 2; i <= length(arr); i++) {
    acc = acc sep arr[i]
  }
  return acc
}

function strip(str) {
  gsub(/^\s+|\s+$/, "", str)
  return str
}

### if not using gawk but awk
# NR == 1 {
#   print "create table " table_name "("
#   str = join(arr, " text,\n")
#   split($0, arr, ",")
#   str = join(arr, " text,\n")
#   print strip(str) " text"
#   print ");"
# }

### FINENAME, FNR are supported only if using gawk
FNR == 1 {
  filename = path_arr[split(FILENAME,path_arr,"/")]
  table_name = substr(filename, 1, index(filename, ".") - 1)
  split($0, arr, ",")
  print "create table " table_name "("
  str = join(arr, " text,\n")
  print strip(str) " text"
  print ");"
}

~ $ awk -f $HOME/.cli/awk/csv2createsql.awk df.csv
create table df(
date text,
source text,
size text,
used text,
avail text,
pcent text,
target text
);
  • 위의 awk script는 gawk에서만 제공되는 FILENAME 변수를 사용한 것에 주의하자.
    • 그냥 awk로는 awk -v table_name=df-f csv2createsql.awk df.csv 같은 형식으로 사용하면 된다
  • 이제 메인 shell file을 만들자 recorddf
#!/bin/bash
df_to_csv_file="$HOME/.cli/awk/df_to_csv.awk"
today=$(date -I)
tmpfile=$(mktemp)

sqlite3 ~/machine.db -cmd "
create table if not exists df(
date text,
source text,
size text,
used text,
avail text,
pcent text,
target text
);
" ".quit"
sqlite3 ~/machine.db -cmd "delete from df where date = '$today'" ".quit"
df | awk -f "$df_to_csv_file" > "$tmpfile"
sqlite3 ~/machine.db \
    -cmd ".mode csv" \
    -cmd ".import --skip 1 $tmpfile df" \
    ".quit"
sqlite3 ~/machine.db -cmd "select count(1) from df where date = '$today'" ".quit"
  • sqlite3-cmd 옵션은 sqlite3로 진입한 interactive 환경에서 실행하는 것과 같다
    • 실제로 위의 shell 파일을 만들 때 interactive 환경에서 여러번 시도하며 작성했다.
  • 이제 위 shell file을 실행가능한 위치에 넣고 PATH에 추가한다
    • export PATH=$PATH:$HOME/.cli/bin
    • cp recorddf $HOME/.cli/bin

대시보드 생성 및 자동 실행

  • 한동안 위의 설정으로 상태를 저장만 해 오다가 어느날 위의 스크립트를 자동으로 실행하고 주된 관심사인 C:\, D:\ 드라이브의 사용량을 bar plot으로 보면 좋겠다는 생각이 들었다
  • 먼저 나의 sqlite3 schema와 저장된 형태를 gpt에게 주고 bar plot을 생성하는 스크립트를 만들어 달라고 했더니 다음과 같은 스크립트가 나왔다
#!/bin/bash
DB="$HOME/machine.db"
TODAY=$(date -I)

ROW=$(sqlite3 "$DB" "
SELECT size, used, pcent
FROM df
WHERE date = '$TODAY'
  AND source = 'C:\\'
LIMIT 1;
")

if [ -n "$ROW" ]; then
    TOTAL=$(echo "$ROW" | cut -d'|' -f1)
    USED=$(echo "$ROW" | cut -d'|' -f2)
    PERCENT=$(echo "$ROW" | cut -d'|' -f3 | tr -d '%')

    BAR_WIDTH=40
    FILLED=$((PERCENT * BAR_WIDTH / 100))
    EMPTY=$((BAR_WIDTH - FILLED))

    BAR=$(printf "%0.s#" $(seq 1 $FILLED))
    SPACE=$(printf "%0.s-" $(seq 1 $EMPTY))

    echo ""
    echo "===== DISK DASHBOARD ====="
    echo "C Drive Usage ($TODAY)"
    printf "[%s%s] %s%%\n" "$BAR" "$SPACE" "$PERCENT"
    printf "Used: %.1fGB / %.1fGB\n" \
        "$(echo "$USED / 1024 / 1024" | bc -l)" \
        "$(echo "$TOTAL / 1024 / 1024" | bc -l)"
    echo ""
fi
  • 정상동작을 확인했으므로, 자동으로 실행하도록 설정만 하면 된다
    • ~/.bashrc에 추가하면 된다
    • $ echo "recorddf 1>/dev/null" >> ~/.bashrc
    • $ echo "bash ~/.cli/bin/diskdashboard" >> ~/.bashrc
  • 이제 리눅스 터미널을 켤 때마다 C드라이브의 사용량이 자동으로 보인다
@widehyo@hackers.pub
  • 리눅스 터미널을 켤 때마다 C드라이브의 사용량이 자동으로 보이는 대시보드를 만들 것이다
~ $ source ~/.bashrc

===== DISK DASHBOARD =====
C Drive Usage (2026-03-08)
[##################################------] 85%
Used: 404.6GB / 476.8GB

발단

  • 필자는 주로 WSL2에서 터미널을 다루고 있는데, 가끔 C:\, D:\ 드라이브에 남은 용량이 없어서 디스크를 정리해야 할 때가 있다.
  • 그럴 때면 주로 작업하는 디렉터리 및 window의 사용자 디렉터리나 %AppData%의 cache 디렉터리로 이동하여 du -sh *로 각 디렉터리 중 불필요하게 큰 용량을 차지하는 부분을 자세히 조사하여 필요 없는 파일을 삭제하곤 한다
  • 삭제 목표는 주로 df의 출력에서 C:\AvailableUse%를 높이는 것으로 설정한다
  • 그러던 어느날 df로 당시의 가용용량을 파악하는 것을 넘어 매일 df의 출력을 저장해 두면 그 추이를 나중에 확인할 때 편할 것이라는 생각이 들었다

전개

df 출력 전처리

  • 자 그럼 시작해보자.
    • awk를 사용할수 있으므로 먼저 df의 출력을 awk로 가공하자
    • 먼저 df의 manual page(man df)를 보면 df의 각 필드에 대한 설명이 나온다
       --output[=FIELD_LIST]
              use the output format defined by FIELD_LIST, or print all fields if FIELD_LIST is omitted.
       FIELD_LIST  is  a  comma-separated list of columns to be included.  Valid field names are: 'source', 'fstype', 'itotal', 'iused', 'iavail', 'ipcent', 'size', 'used', 'avail', 'pcent', 'file' and 'target' (see info page).
  • df의 기본 출력이 어떤 field를 보여주는지 --output을 통해 검증하자
~ $ df
Filesystem      1K-blocks      Used Available Use% Mounted on
drivers         499952636 424267596  75685040  85% /usr/lib/wsl/drivers
/dev/sdd       1055762868 134215988 867843408  14% /
C:\             499952636 424267596  75685040  85% /mnt/c
snapfuse              128       128         0 100% /snap/bare/5
snapfuse           340224    340224         0 100% /snap/code/219
snapfuse            65408     65408         0 100% /snap/core20/2686
snapfuse            75776     75776         0 100% /snap/core22/2216
tmpfs              800380         8    800372   1% /run/user/1000
...
~ $ df --output=source,size,used,avail,pcent,target
Filesystem      1K-blocks      Used     Avail Use% Mounted on
drivers         499952636 424269376  75683260  85% /usr/lib/wsl/drivers
/dev/sdd       1055762868 134216048 867843348  14% /
C:\             499952636 424269376  75683260  85% /mnt/c
snapfuse              128       128         0 100% /snap/bare/5
snapfuse           340224    340224         0 100% /snap/code/219
snapfuse            65408     65408         0 100% /snap/core20/2686
snapfuse            75776     75776         0 100% /snap/core22/2216
tmpfs              800380         8    800372   1% /run/user/1000
...
  • df의 기본 출력은 source,size,used,avail,pcent,target 필드라는 것을 알 수 있다.
  • df의 output은 형식을 가진 문자열(formatted string)이므로, csv 형태로 가공하는 awk 스크립트를 만들자
function join(arr, sep) {
  acc = arr[1]
  for (i = 2; i <= length(arr); i++) {
    acc = acc sep arr[i]
  }
  return acc
}

NR == 1 {
  cmd = "date -I"
  cmd | getline date
  close(cmd)
  print "date,source,size,used,avail,pcent,target"
  next
}

{
  split($0, arr)
  str = join(arr, ",")
  printf "%s,%s\n",date,str
}
  • YYYY-MM-DD 형식을 만드는 가장 편한 방법은 date -I를 이용하는 것이다
    • awk 안에서 bash command를 파이프와 getline 그리고 출력을 받는 변수로 연결하면 output을 변수에 저장할수 있다
    • 이 방법을 사용할 때는 반드시 command를 닫도록 주의하자
  • 각 필드를 ","로 join하는 방법은 printf "%s,%s,...",$1,$2,... 도 있지만 $0을 한번 더 split하여 arr로 만든 후 join하는 방법도 있다. 여기서는 후자를 채택하였다
~ $ df | awk -f $HOME/.cli/awk/df_to_csv.awk
date,source,size,used,avail,pcent,target
2026-03-08,drivers,499952636,424269604,75683032,85%,/usr/lib/wsl/drivers
2026-03-08,/dev/sdd,1055762868,134216088,867843308,14%,/
2026-03-08,C:\,499952636,424269604,75683032,85%,/mnt/c
2026-03-08,snapfuse,128,128,0,100%,/snap/bare/5
2026-03-08,snapfuse,340224,340224,0,100%,/snap/code/219
2026-03-08,snapfuse,65408,65408,0,100%,/snap/core20/2686
2026-03-08,snapfuse,75776,75776,0,100%,/snap/core22/2216
2026-03-08,tmpfs,800380,8,800372,1%,/run/user/1000
...

DB 저장

  • 이제 이 출력을 어딘가에 저장해야 한다
    • 하루에 최대 한번만 snapshot을 저장하고 레코드가 누적되는 구조이기 때문에 database가 성격에 맞다
    • 그렇다고 외부에서 접속하는 기능은 필요 없으므로 postgresql씩은 필요 없을 것 같다
    • 어디까지나 local machine에 대한 상태를 DB로 관리하기 위함이니 local에서 파일로 관리되는 sqlite3duckdb가 성격에 맞을 것 같다
    • 사용하는 명령어의 성격이 bash native에 가까워 duckdb까지 의존성을 추가할 필요는 없어 sqlite3를 채택한다
  • db path는 ~/machine.db로 정했다. 목적을 잘 반영하는 것 같다.
    • 전략은 sqlite의 .mode csv .import /path/to/target.csv target_table를 이용해 csv 내용을 table에 넣는 것이다
  • 위에서 만든 df | awk -f $HOME/.cli/awk/df_to_csv.awk의 출력을 이용해 만든 csvsql로 변환하자
$ df | awk -f $HOME/.cli/awk/df_to_csv.awk > ~/df.csv
  • csv의 header만 읽어 table을 create하는 sql로 전환하는 awk는 만들어두면 사용할 일이 많을 것 같다
$ awk -f $HOME/.cli/awk/csv2createsql.awk ~/df.csv
$ cat $HOME/.cli/awk/csv2createsql.awk
function join(arr, sep) {
  acc = arr[1]
  for (i = 2; i <= length(arr); i++) {
    acc = acc sep arr[i]
  }
  return acc
}

function strip(str) {
  gsub(/^\s+|\s+$/, "", str)
  return str
}

### if not using gawk but awk
# NR == 1 {
#   print "create table " table_name "("
#   str = join(arr, " text,\n")
#   split($0, arr, ",")
#   str = join(arr, " text,\n")
#   print strip(str) " text"
#   print ");"
# }

### FINENAME, FNR are supported only if using gawk
FNR == 1 {
  filename = path_arr[split(FILENAME,path_arr,"/")]
  table_name = substr(filename, 1, index(filename, ".") - 1)
  split($0, arr, ",")
  print "create table " table_name "("
  str = join(arr, " text,\n")
  print strip(str) " text"
  print ");"
}

~ $ awk -f $HOME/.cli/awk/csv2createsql.awk df.csv
create table df(
date text,
source text,
size text,
used text,
avail text,
pcent text,
target text
);
  • 위의 awk script는 gawk에서만 제공되는 FILENAME 변수를 사용한 것에 주의하자.
    • 그냥 awk로는 awk -v table_name=df-f csv2createsql.awk df.csv 같은 형식으로 사용하면 된다
  • 이제 메인 shell file을 만들자 recorddf
#!/bin/bash
df_to_csv_file="$HOME/.cli/awk/df_to_csv.awk"
today=$(date -I)
tmpfile=$(mktemp)

sqlite3 ~/machine.db -cmd "
create table if not exists df(
date text,
source text,
size text,
used text,
avail text,
pcent text,
target text
);
" ".quit"
sqlite3 ~/machine.db -cmd "delete from df where date = '$today'" ".quit"
df | awk -f "$df_to_csv_file" > "$tmpfile"
sqlite3 ~/machine.db \
    -cmd ".mode csv" \
    -cmd ".import --skip 1 $tmpfile df" \
    ".quit"
sqlite3 ~/machine.db -cmd "select count(1) from df where date = '$today'" ".quit"
  • sqlite3-cmd 옵션은 sqlite3로 진입한 interactive 환경에서 실행하는 것과 같다
    • 실제로 위의 shell 파일을 만들 때 interactive 환경에서 여러번 시도하며 작성했다.
  • 이제 위 shell file을 실행가능한 위치에 넣고 PATH에 추가한다
    • export PATH=$PATH:$HOME/.cli/bin
    • cp recorddf $HOME/.cli/bin

대시보드 생성 및 자동 실행

  • 한동안 위의 설정으로 상태를 저장만 해 오다가 어느날 위의 스크립트를 자동으로 실행하고 주된 관심사인 C:\, D:\ 드라이브의 사용량을 bar plot으로 보면 좋겠다는 생각이 들었다
  • 먼저 나의 sqlite3 schema와 저장된 형태를 gpt에게 주고 bar plot을 생성하는 스크립트를 만들어 달라고 했더니 다음과 같은 스크립트가 나왔다
#!/bin/bash
DB="$HOME/machine.db"
TODAY=$(date -I)

ROW=$(sqlite3 "$DB" "
SELECT size, used, pcent
FROM df
WHERE date = '$TODAY'
  AND source = 'C:\\'
LIMIT 1;
")

if [ -n "$ROW" ]; then
    TOTAL=$(echo "$ROW" | cut -d'|' -f1)
    USED=$(echo "$ROW" | cut -d'|' -f2)
    PERCENT=$(echo "$ROW" | cut -d'|' -f3 | tr -d '%')

    BAR_WIDTH=40
    FILLED=$((PERCENT * BAR_WIDTH / 100))
    EMPTY=$((BAR_WIDTH - FILLED))

    BAR=$(printf "%0.s#" $(seq 1 $FILLED))
    SPACE=$(printf "%0.s-" $(seq 1 $EMPTY))

    echo ""
    echo "===== DISK DASHBOARD ====="
    echo "C Drive Usage ($TODAY)"
    printf "[%s%s] %s%%\n" "$BAR" "$SPACE" "$PERCENT"
    printf "Used: %.1fGB / %.1fGB\n" \
        "$(echo "$USED / 1024 / 1024" | bc -l)" \
        "$(echo "$TOTAL / 1024 / 1024" | bc -l)"
    echo ""
fi
  • 정상동작을 확인했으므로, 자동으로 실행하도록 설정만 하면 된다
    • ~/.bashrc에 추가하면 된다
    • $ echo "recorddf 1>/dev/null" >> ~/.bashrc
    • $ echo "bash ~/.cli/bin/diskdashboard" >> ~/.bashrc
  • 이제 리눅스 터미널을 켤 때마다 C드라이브의 사용량이 자동으로 보인다
@ThierryStoehr@framapiaf.org · Reply to Lutin Discret

@lutindiscret Je me permets de répondre en partant du moteur DuckDuckGo, mais avec l'URL noai.duckduckgo.com/ Et son indique le "NO AI"... qui permet (a priori) d'écarter le slop.
Avec les articles de Wikipedia, les pages de man ou de info, comme par exemple avec "man groff" ou de "info groff" (car groff est la commande et le logiciel derrière les pages de manuel pour formater son contenu... mais c'est hors sujet, désolé). Et il y a aussi le pour trouver ici. "Hope it helps" -)

duckduckgo.com

DuckDuckGo - Protection. Privacy. Peace of mind.

The Internet privacy company that empowers you to seamlessly take control of your personal information online, without any tradeoffs.

@nev@flipping.rocks · Reply to neville park
helpful bash function for shrinking video with ffmpeg

By the way, quickly turned this from a 150 MB video to an 11 MB one with a handy bash function (with built-in help option because I can never even remember how this simplified version goes). Width is in pixels.

Example: "video-scale video.mp4 800 video_small.mp4"

function video-scale () {
case "$1" in
-h|--help)
echo "Usage: video-scale INPUT-FILE WIDTH OUTPUT-FILE"
;;
*)
ffmpeg -hide_banner -i "$1" -filter:v scale="$2":-2 -c:a copy "${3}"
;;
esac

@ai4msfcharity@ieji.de · Reply to Lobsters

That looks really handy\! I've been building dev CLI tools too — there's something satisfying about tools that just work in the terminal without needing IDE plugins.

Have you tried combining it with structured output? We built a free API that does code formatting, hashing, and text analysis — all via curl, no setup needed.

@kkarhan@infosec.space · Reply to alina🏳️‍⚧️🐾

@alina personally I can also recommend good tools that just work.

And many more.

github.com

pkgs/docs/WISHLIST.tsv at main · OS-1337/pkgs

OS/1337 Package Repository. Contribute to OS-1337/pkgs development by creating an account on GitHub.

@zenn_trend_bot@silicon.moe · Reply to Zenn Trends

📰 bump というバージョンバンプツールを作った (👍 35)

🇬🇧 Created 'bump': a universal version bumping tool supporting multiple languages (Go, Rust, Python, etc.) with semantic versioning. Inspired by gobump.
🇰🇷 'bump' 개발: Go, Rust, Python 등 여러 언어를 지원하는 범용 버전 범핑 도구. 시맨틱 버저닝 적용. gobump에서 영감을 받음.

🔗 zenn.dev/mattn/articles/28ff86

zenn.dev

bump というバージョンバンプツールを作った

@tommi@pan.rent

💾 The File Count Challenge 🔎

Have you ever wondered how many PDFs, images, videos, or particular kind of files you have on your device? It is the time to

I am hereby proud to officially launch a Fedi-wide file count challenge, in exclusive partnership with the 2025-27 class of @xpub 👾

💁 How to participate

  1. Go to your main device (not mobile)
  2. Open the terminal
  3. Run find . | grep '.pdf$' -c (Windows equivalent suggestions welcome)
  4. Repeat the previous command, replacing “pdf” with any file format you are interested in sharing. We suggest: jpg, mp3, and png. 🔆 Bonus: html, js, ttf, odt, svg
  5. Take a screenshot of the output
  6. Share the screenshot on the Fediverse, using the hashtag (please, copy-paste the actual text from the terminal in the image description/alt text)
  7. Add your (user)name and results to the scoreboard
  8. 🆒 Bonus: share the story explaining why you have so many/little files for one format.

🏆 Awards

There is a secret prize for the three people who have the most PDFs, JPEGs, and HTMLs. More info to follow.

Boost, share, fork… Do whatever you want, but please have fun while doing it.

I willl keep writing updates. Yaaaaaaay ⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️⌨️

find | grep '.ttf$' -c
427

find | grep '.odt$' -c
637

find | grep '.svg$' -c
3737

find | grep '.mp3$' -c
150

find | grep '.js$' -c
38819

find | grep '.png$' -c
19199
ALT text

find | grep '.ttf$' -c 427 find | grep '.odt$' -c 637 find | grep '.svg$' -c 3737 find | grep '.mp3$' -c 150 find | grep '.js$' -c 38819 find | grep '.png$' -c 19199

find | grep '.pdf$' -c
2676

find | grep '.html$' -c
7292

find | grep '.md$' -c
8093

find | grep '.mp4$' -c
1386

find | grep '.jpg$' -c
20561

find | grep '.jxl$' -c
35

find | grep '.ttf$' -c
427
ALT text

find | grep '.pdf$' -c 2676 find | grep '.html$' -c 7292 find | grep '.md$' -c 8093 find | grep '.mp4$' -c 1386 find | grep '.jpg$' -c 20561 find | grep '.jxl$' -c 35 find | grep '.ttf$' -c 427

@toxi@mastodon.thi.ng

Also new in : The new thi.ng/text-format-image package provides conversion/formatting for bitmap output for CLI/Terminal apps, currently only via the widely supported iTerm2 format (see readme for details). Supports JPG/PNG (possibly others, depending on terminal used) as well as thi.ng/pixel pixel buffers (e.g. for dynamically generated images/visualizations)

Ps. The above relies on extended ANSI sequences to submit bitmap data to the terminal. If you're after actual text/character-based image conversion, you can alternatively use the functions provided in thi.ng/text-canvas:

github.com/thi-ng/umbrella/tre

Screenshot of a CLI/Terminal with the code example from the thi.ng/text-format-image readme and the resulting image output as part of the normal flow...
ALT text

Screenshot of a CLI/Terminal with the code example from the thi.ng/text-format-image readme and the resulting image output as part of the normal flow...

@ploum@mamot.fr

I’m trying to use xargs to evaluate a mathematical expression.

Like:

echo "1234" | xargs -I{} echo $(({}+1))

But, for whatever reason, {} is always evaluated as "0" (while I can confirm those are integer).

Any hint? Maybe something else than xargs for the job here?

@WeirdWriter@caneandable.social
@hongminhee@hollo.social

0.9.0 is here!

This release brings /await support to parsers. Now you can validate input against external resources—databases, APIs, Git repositories—directly at parse time, with full type safety.

The new @optique/git package showcases this: validate branch names, tags, and commit SHAs against an actual Git repo, complete with shell completion suggestions.

Other highlights:

  • Hidden option support for deprecated/internal flags
  • Numeric choices in choice()
  • Security fix for shell completion scripts

Fully backward compatible—your existing parsers work unchanged.

https://github.com/dahlia/optique/discussions/75

github.com

Optique 0.9.0: Async parsers and Git reference validation · dahlia/optique · Discussion #75

We're excited to announce Optique 0.9.0! This release brings two major features: full async/await support for parsers and a new @optique/git package for validating Git references against actual rep...

@hongminhee@hollo.social

0.9.0 is here!

This release brings /await support to parsers. Now you can validate input against external resources—databases, APIs, Git repositories—directly at parse time, with full type safety.

The new @optique/git package showcases this: validate branch names, tags, and commit SHAs against an actual Git repo, complete with shell completion suggestions.

Other highlights:

  • Hidden option support for deprecated/internal flags
  • Numeric choices in choice()
  • Security fix for shell completion scripts

Fully backward compatible—your existing parsers work unchanged.

https://github.com/dahlia/optique/discussions/75

github.com

Optique 0.9.0: Async parsers and Git reference validation · dahlia/optique · Discussion #75

We're excited to announce Optique 0.9.0! This release brings two major features: full async/await support for parsers and a new @optique/git package for validating Git references against actual rep...

@hongminhee@hollo.social

0.9.0 is here!

This release brings /await support to parsers. Now you can validate input against external resources—databases, APIs, Git repositories—directly at parse time, with full type safety.

The new @optique/git package showcases this: validate branch names, tags, and commit SHAs against an actual Git repo, complete with shell completion suggestions.

Other highlights:

  • Hidden option support for deprecated/internal flags
  • Numeric choices in choice()
  • Security fix for shell completion scripts

Fully backward compatible—your existing parsers work unchanged.

https://github.com/dahlia/optique/discussions/75

github.com

Optique 0.9.0: Async parsers and Git reference validation · dahlia/optique · Discussion #75

We're excited to announce Optique 0.9.0! This release brings two major features: full async/await support for parsers and a new @optique/git package for validating Git references against actual rep...

@hugovk@mastodon.social · Reply to Hugo van Kemenade
@hugovk@mastodon.social · Reply to Hugo van Kemenade

Just released! 🚀

pypistats 1.12.0

CLI for PyPI download stats

support 3.15
drop 3.9
improve verbose output
declare type hints
replace dateutil+six dependencies with stdlib
replace httpx with urllib
replace pre-commit with prek

github.com/hugovk/pypistats/re

Release 1.12.0 · hugovk/pypistats

Added Add support for Python 3.15 (#513) @hugovk Include human URL in verbose output (#499) @hugovk Add py.typed file and "Typing :: Typed" classifier (#523) @hugovk Changed Drop support for Pyt...

@rolle@mementomori.social

Just released by me: OmniShuffle - A unified command-line music shuffler built with python, it combines Spotify, Pandora, and YouTube Music into a single streaming experience with pianobar-style controls and Last.fm scrobbling support.

This will eventually replace my pianobar-setup I've used for 10+ years. github.com/ronilaukkarinen/pia

Source code: github.com/ronilaukkarinen/omn

A CLI music player of mine
ALT text

A CLI music player of mine

@rolle@mementomori.social

Just released by me: OmniShuffle - A unified command-line music shuffler built with python, it combines Spotify, Pandora, and YouTube Music into a single streaming experience with pianobar-style controls and Last.fm scrobbling support.

This will eventually replace my pianobar-setup I've used for 10+ years. github.com/ronilaukkarinen/pia

Source code: github.com/ronilaukkarinen/omn

A CLI music player of mine
ALT text

A CLI music player of mine

@orhun@fosstodon.org

Ever felt lost in a Markdown file? It's not happening again!

🌳 **treemd** — An interactive Markdown navigator with a collapsible heading tree.

💯 Read Markdown like code!

🧭 Dual-pane interface with vim-style navigation, live search & synchronized scrolling.

🦀 Written in Rust & built with @ratatui_rs

⭐ GitHub: github.com/Epistates/treemd

@itsfoss@mastodon.social

A stopwatch in your terminal! How cool is that? 😎

Quick Terminal Tip

A simple and useful stopwatch

You can run a simple stopwatch in your terminal using:

time read

Press Ctrl + D to stop.
ALT text

Quick Terminal Tip A simple and useful stopwatch You can run a simple stopwatch in your terminal using: time read Press Ctrl + D to stop.

@hugovk@mastodon.social

Use Trusted Publishing instead of long-lived PyPI tokens. For other things, here's how to use 1Password with direnv to set secrets in env vars.
hugovk.dev/blog/2025/secrets-i

hugovk.dev

Setting secrets in env vars

@miketheman@hachyderm.io

There's a nasty worm going around named Shai-Hulud. It's also capable of exposing some projects' long-lived PyPI API Tokens. Read more on what's happening, and what you can do to protect your projects.

TL,DR: Adopt Trusted Publishing 🔐🚀📦

blog.pypi.org/posts/2025-11-26

blog.pypi.org

PyPI and Shai-Hulud: Staying Secure Amid Emerging Threats - The Python Package Index Blog

Shai-Hulud is a great worm, not yet a snake. Attack on npm ecosystem may have implications for PyPI.

@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@hongminhee@hollo.social
@deadtom@dice.camp
The title reads "Linux terminal for beginners", with a photo of an orange cat below. There are arrows pointing to different parts of the cat saying "head", "cat", "tail".
ALT text

The title reads "Linux terminal for beginners", with a photo of an orange cat below. There are arrows pointing to different parts of the cat saying "head", "cat", "tail".

@deadtom@dice.camp
The title reads "Linux terminal for beginners", with a photo of an orange cat below. There are arrows pointing to different parts of the cat saying "head", "cat", "tail".
ALT text

The title reads "Linux terminal for beginners", with a photo of an orange cat below. There are arrows pointing to different parts of the cat saying "head", "cat", "tail".

@deadtom@dice.camp
The title reads "Linux terminal for beginners", with a photo of an orange cat below. There are arrows pointing to different parts of the cat saying "head", "cat", "tail".
ALT text

The title reads "Linux terminal for beginners", with a photo of an orange cat below. There are arrows pointing to different parts of the cat saying "head", "cat", "tail".

@deadtom@dice.camp
The title reads "Linux terminal for beginners", with a photo of an orange cat below. There are arrows pointing to different parts of the cat saying "head", "cat", "tail".
ALT text

The title reads "Linux terminal for beginners", with a photo of an orange cat below. There are arrows pointing to different parts of the cat saying "head", "cat", "tail".

@nev@status.nevillepark.ca

Anyone on Android 15+ managed to ssh into a local Linux box from the native terminal app now in Android?

Once again, this is using the phone to ssh into a computer. Not the other way round.

I could easily ssh into my account on tty.sdf.org, but it just hangs forever when I try accessing my laptop. Got sshd running, but I've likely set something up wrong.

✅ 📱→ 💻
❌ 💻 → 📱

#android #ssh #terminal #CommandLine #cli

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

Exciting news for developers! We've just landed a major milestone for Fedify 2.0—the now runs natively on .js and , not just (#456). If you install @fedify/cli@2.0.0-dev.1761 from npm, you'll get actual JavaScript that executes directly in your runtime, no more pre-compiled binaries from deno compile. This is part of our broader transition to Optique, a new cross-runtime CLI framework we've developed specifically for Fedify's needs (#374).

This change means a more natural development experience regardless of your runtime preference. Node.js developers can now run the CLI tools directly through their familiar ecosystem, and the same goes for Bun users. While Fedify 2.0 isn't released yet, we're excited to share this progress with the community—feel free to try out the dev version and let us know how it works for you!

github.com

CLI for Fedify 2.0: Cross-runtime transition and Optique adoption · Issue #374 · fedify-dev/fedify

The current Fedify CLI implementation has been working well for our Deno-focused ecosystem, but as we prepare for Fedify 2.0, we're hitting some architectural limitations that are becoming increasi...

@widehyo@hackers.pub

요새 awk에 대해 많은 관심을 가지고 있다. 특히 awk는 기본적으로 제공되는 feature가 가장 적은 언어중에 하나이기 때문에 다른 언어에서 편하게 사용했던 편의 기능을 직접 구현해서 사용해야 하는 경우가 많다. 하지만 awk 특유의 script스러움과 여러 편의기능 및 문법은 이에 익숙해진 사용자에게 빠져나가기 힘든 매력을 지니고 있기도 하다.

서론은 여기까지 하고 상당히 복잡한 구현이었던 functools.partial을 awk로 구현한 내용을 자세히 살펴보자.

먼저, awk에 대한 이해를 돕기 위해 언어가 가진 제약사항을 먼저 언급하고 가자.

  1. 모든 자료형은 string, number, or array(associative array)이다.
  2. 데이터를 표현하는 class나 struct를 제공하지 않는다.
  3. nested funciton을 지원하지 않는다.
  4. 따라서 closure를 지원하지 않는다.
  5. array를 return할 수 없다.
  6. multi return이 불가능하다.
  7. function pointer를 사용할 수 없다 (void *)를 이용한 함수 객체 활용이 불가하다.
  8. 변수는 기본적으로 global variable이다.
  9. (gawk가 아닌 awk 한정) multi dimensional array를 지원하지 않는다.
  10. 런타임에서 reflection이나 inspect를 할 수 있는 도구가 제공되지 않는다.

위의 제약사항 중 3, 4, 5, 6번은 C언어가 가진 제약사항을 고려하면 이해가 된다. 그러나 1번과 2번, 그리고 7번은 C언어 보다도 강력한 제약사항이라고 볼 수 있다. 그나마 7번은 gawk(GNU awk) 5.1 버전에서는 functionName = "myfunc"; @functionName을 이용하여 간접호출은 가능하다. 하지만 함수를 반환하거나, 변수에 함수를 할당하거나 파라미터에 함수를 넘기는 것은 불가능하다. 8번의 경우는 그나마 C언어의 함수 스코프가 지원되는 것을 이용하면 함수 내에서 지역변수 취급하고 싶은 변수를 parameter 자리에 넣음으로써 지역변수 취급이 가능하다.

그리고 이를 극복하기 위한 대응방안은 다음과 같다.

1번과 2번의 사용자정의 자료형 문제는 자료형을 표현할 수 있는 문자열을 설계(serialization과 같은 전략)하여 전달하다가 필요한 시점에 해당 문자열을 다시 원하는 형태로 복원하는 방법으로 극복할 수 있다. 이 전략은 posix awk가 multi dimensional array를 지원하지 않는 것에 대한 대응방안으로 a[i][j] 대신 a[i, j]로 사용한 점에서 착안했다. 편의를 위한 배열 인덱스 자리에 위치하는 i, j는 사실 "i\034j" 문자와 같다. 배열의 index에 위치하는 ,는 키보드로 입력 불가능한 문자인 "\034"로 치환되며, awk에서의 강력한 사용성을 지원하기 위해 내장변수 SUBSEP으로 \034 문자를 사용할 수 있다.

3번과 4번의 경우는 함수형 프로그래밍 방식을 선호하는 필자에게는 많은 불편함을 가져다 주었고, 이번 포스팅의 주된 도전과제였다. 간략히 극복전략을 이야기하자면 global table(associative array)를 이용하여 storage에 원하는 내용을 넣었다가 사용하는 방식으로 극복하였다.

5번과 6번은 생각보다 극복 난이도가 낮았는데, C언어의 call by reference 방식으로 우회하면 된다. C언어에서 array를 sort하기 위해 배열의 포인터를 함수에 넘기고 안에서 swap한 것과 근본적으로 같은 방식이다. 한편, multi return을 위해 새로운 배열을 만들어 return하고 싶을 때는 해당 배열을 parameter로 넘기고 함수 본문 시작시 delete 문을 이용해 우연히 같은 이름을 사용하는 global variable 문제를 방지하였다. 그리고 이 방법은 awk의 기본 함수인 split("text", "array", "separator")에서 자연스럽게 사용하는 방법이기 때문에 awk의 idiom과도 부합한다.

7번은 gawk의 힘을 빌려 @fn 을 이용한다. man awk를 읽다가 @fn을 보고 이거 잘하면 함수형 프로그래밍 할 수 있겠는데 라는 생각이 이 글의 근본적인 모티베이션이 되었다. 하지만 10번의 제약사항에 의하여 런타임에서 dynamic하게 함수/매개변수에 접근할 수 없었기 때문에 debugger가 참조하는 프레임 객체와 비슷한 것 중 functools.partial에서 필요한 정보만 담은 global variable을 직접 만들어 구현했다.

가장 먼저 진행한 부분은 함수 signature를 저장하는 방법을 찾는 것이었다. functools.partial과 같은 함수는 parameter의 위치 및 key를 이용하여 default parameter를 binding할 수 있었기 때문에 해당하는 정보를 저장해야 했다. 결론만 말하자면 다음과 같은 함수가 있을 때

function sample(a, b, c) { ... }

[(a, 1), (b, 2), (c, 3)] 과 같은 형태로 함수의 signature가 저장되기를 원했다. 하지만 array는 element로 array를 가질 수 없기에(array의 원소에 array를 할당하려 하면 syntax error), [(a, 1), (b, 2), (c, 3)]의 string representation을 다음과 같이 저장했다.

SSEP = "\035"
"1" SUBSEP "a" SSEP "2" SUBSEP "b" SSEP "3" SUBSEP "c"

구분자가 2개가 필요했는데 하나는 튜플 안에서 데이터를 구분하는 구분자로 사용된 SUBSEP, 그리고 리스트에서 각 튜플의 구분자로 사용된 SSEP이다. 이렇게 function signature를 string으로 표현하여 global variable인 FUNCSIG 테이블에 함수명을 키로, function signature의 string repr을 value로 저장한다. 그러면 대충 python으로 보면 이런 느낌이 된다.

FUNCSIG["sample"] = "1\034a\0352\034b\0353\034c"

그리고 실제로 사용할 때는 SSEP과 SUBSEP으로 split하여(deserialize하여) {"a":1,"b":2,"c":3} 과 같은 형태로 사용한다.

# save function signature to global variable FUNCSIG
# argument information is of form list of tuples (position, parameter name)
# list element delemter: SSEP, key-value delemeter: SUBSEP
function saveSignature(fnname, argInfo) {
  posRepr = bindPosition(argInfo)
  FUNCSIG[fnname] = posRepr
}

function bindPosition(array,    acc) {
  for (i = 1; i <= length(array); i++) {
    acc = acc i SUBSEP array[i] SSEP
  }
  return substr(acc, 1, length(acc) - 1)
}

두번째로는 진행한 함수는 binding된 함수를 만드는 함수이다. functools의 partial이 하는 역할과 같다고 보면 된다.

원본함수명(fnname)과 default parameter의 정보(bindings)를 받아 원본 함수의 parameter 중 명시적으로 호출하지 않은 매개변수는 bindings로 미리 정의한 매개변수를 사용하는 함수의 이름을 key로 등록하는 동작을 awk용으로 바꾼 것이다.

>>> from functools import partial
>>> def fnname(a, b):
...     print(f"a: {a}, b: {b}")
... 
>>> key = partial(fnname, b=3)
>>> key(1)
a: 1, b: 3
function fnname(a, b) {
    printf "a: %s, b: %s", a, b
}
# 정의부
key = "partial_fn"
bind(key, "fnname", "b" SUBSEP "3") # partial_fn = partial(fnname, b=3)과 같은 역할

# 사용부
call(key, "a" SUBSEP "1") # partial_fn(a=1)과 같은 역할 == @key(a=1)과 같은 역할

매개변수는 argument를 binding하는 정보를 담고 있는 array(bindings)와 원본 함수명(fnname) 그리고 binding한 함수명(key)으로 구성된다.

함수명을 통한 함수 간접호출은 gawk에서 @fn 문법으로 가능하기 때문에, 어떤 함수에 어떤 parameter가 바인딩 되었는지를 정보로 전달하고 binding된 새로운 함수를 key로 global variable BINDING이 가지고 있다가 call 함수에 의해 해당 정보를 찾아 reconstruction을 통해 호출하는 방법이다. 최대한의 유연성을 확보하기 위하여 bindings는 associative array로 설계했고, 위에서 저장한 function signature와 실제 실행 책임을 가진 함수 call에서 호출할 args와 함께 사용된다.

binding한 정보는 [원본함수명] [(매개변수1, 값1), (매개변수2, 값2), ...] 와 같은 형태로 저장된다. 물론, awk에는 string과 number만 primitive type으로 가지므로 string representation 형태로 저장한다.

# save binding arguments to global variable BINDING
# key will be used as partial function(default parameter from binding info)
# the form value consists two parts, head and tails separated with first SSEP
# head contains `original function name`
# tail is of form string representation of the list of tuple, which argName SUBSEP value
# each elements is joined with SSEP
function bind(key, fnname, bindings,    argRepr) {
  argRepr = fnname SSEP

  if (length(bindings) == 0) {
    BINDING[key] = argRepr
    return
  }
  argRepr = argRepr serializeAssoc(bindings)
  BINDING[key] = argRepr
}

헬퍼 함수 serializeAssoc을 사용했다.

function serializeAssoc(assoc,    acc) {
  for (key in assoc) {
    acc = acc key SUBSEP assoc[key] SSEP
  }
  return substr(acc, 1, length(acc) - 1)
}

위에서 만든 바인딩된 함수를 호출하는 함수 call이다. 실제 함수호출을 담당하고 내부로직이 가장 복잡한 함수이다. function signature는 다음과 같다.

function call(key, args,    bindings)

위에서 정의한 key로 BINDING에서 argRepr을 가져온다. argRepr의 첫 번째 위치에 있는 함수명을 추출한다 추출한 함수명으로 FUNCSIG에 저장된 posRepr를 가져온다.

argRepr의 두 번째 위치에 있는 binding 정보를 복원하고 원본함수의 function signature를 담고 있는 posRepr을 각각 복원하여, 실제로 호출할 args와 결합하여 호출한다.

전반부는 실제 파라미터를 생성하는 부분으로 구성된다.

function call(key, args,    bindings) {
  if (!BINDING[key]) {
    print "unregistered function call"
    return
  }
  buildParameter(key, args, params)

buildParameter 함수는 바인딩된 함수를 호출할 args를 우선적으로 적용하고 args에서 채워지지 않은 매개변수는 bindingRepr에서 표현된 매개변수 정보에서 채워넣는다. 만약 모든 parameter가 채워지지 않았다면 에러를 출력한다. multi return을 위하여 params 배열을 이용한다.

function buildParameter(key, args, params) {
  partition(BINDING[key], SSEP, fnBinding)
  fnname = fnBinding[1]
  inspectParams(fnname, paramDict)
  bindingRepr = fnBinding[2]
  bindingRepr2Dict(bindingRepr, bindingDict)

  for (idx in args) {
    params[idx] = args[idx]
  }
  for (idx in paramDict) {
    if (!params[paramDict[idx]]) {
      params[paramDict[idx]] = bindingDict[idx]
    }
  }
  if (length(paramDict) != length(params)) {
    print "invalid execution, mismatch between original function and binded function"
    delete params
  }
}

partition은 헬퍼함수이다. separator를 기준으로 전후를 잘라낸다. 역시 multi return이 필요하기 때문에 headtail 매개변수 배열을 이용한다.

function partition(str, sep, headtail) {
  headtail[1] = substr(str, 1, index(str, sep) - length(sep))
  headtail[2] = substr(str, index(str, sep) + length(sep))
}

bindingRepr2Dict는 string representation을 dictionary(associative array)로 복원하는 함수이다. 내용이 간단하므로 먼저 소개한다. BINDING global 변수와 관련있다.

function bindingRepr2Dict(repr, bindingDict,    tmp) {
  split(repr, tmp, SSEP)
  for (idx in tmp) {
    split(tmp[idx], keyVal, SUBSEP)
    bindingDict[keyVal[1]] = keyVal[2]
  }
}

inspectParams는 원본 함수의 function signature를 담고 있는 FUNCSIG global 변수와 관련있다.

function inspectParams(fnname, paramDict) {
  posRepr = FUNCSIG[fnname]
  posRepr2Dict(posRepr, paramDict)
}

function posRepr2Dict(posRepr, paramDict,    tmp, posNameTuple) {
  split(posRepr, tmp, SSEP)
  for (idx in tmp) {
    split(tmp[idx], posName, SUBSEP)
    paramDict[posName[2]] = posName[1]
  }
}

아래 부분이 비즈니스 로직으로, 명시적으로 호출하는 매개변수를 이용해 원본 함수의 funciton signature를 최대한 채워 넣은 후 비어있는 매개변수에 대하여 bindingDict에서 정의되어 있다면 나머지를 채워 넣는 식으로 작동한다.

  for (idx in args) {
    params[idx] = args[idx]
  }
  for (idx in paramDict) {
    if (!params[paramDict[idx]]) {
      params[paramDict[idx]] = bindingDict[idx]
    }
  }

물론, 명시적으로 주어진 parameter와 binding된 매개변수 모두 사용해도 함수가 요구하는 매개변수를 모두 채우지 못한다면 에러메시지를 출력한다.

  if (length(paramDict) != length(params)) {
    print "invalid execution, mismatch between original function and binded function"
    delete params
  }

사실 위의 코드에서는 exit로 나가도 좋다

이렇게 parameter를 구성하면 남은 작업은 간단하다. 매개변수의 개수에 따라 해당하는 함수를 동적으로 호출하는 부분만 남았다. call 함수의 후반부 코드를 보자.


  switch (length(params)) {
    case 1:
      executeFunc1(fnname, params)
      break
    case 2:
      executeFunc2(fnname, params)
      break
    case 3:
      executeFunc3(fnname, params)
      break
    case 4:
      executeFunc4(fnname, params)
      break
    default:
      print "#(function params) must one of 1, 2, 3, or 4"
      return
  }

여러 helper 함수를 사용하는데, 이것은 awk가 동적 프로그래밍이 불가능하기 때문이다. 쉽게 말하면 eval이 없어서 각종 템플릿에 맞는 함수를 하나씩 사전에 정의해 두어야 한다.


function executeFunc1(fnname, params) {
  @fnname(params[1])
}

function executeFunc2(fnname, params) {
  @fnname(params[1], params[2])
}

function executeFunc3(fnname, params) {
  @fnname(params[1], params[2], params[3])
}

function executeFunc4(fnname, params) {
  @fnname(params[1], params[2], params[3], params[4])
}

이상을 모두 적용한 전체 코드는 다음과 같다.


function sample(a, b, c) {
  printf "a: %s, b: %s, c: %s\n", a, b, c
}

# save binding arguments to global variable BINDING
# key will be used as partial function(default parameter from binding info)
# the form value consists two parts, head and tails separated with first SSEP
# head contains `original function name`
# tail is of form string representation of the list of tuple, which argName SUBSEP value
# each elements is joined with SSEP
function bind(key, fnname, bindings,    argRepr) {
  argRepr = fnname SSEP

  if (length(bindings) == 0) {
    BINDING[key] = argRepr
    return
  }
  argRepr = argRepr serializeAssoc(bindings)
  BINDING[key] = argRepr
}

function serializeAssoc(assoc,    acc) {
  for (key in assoc) {
    acc = acc key SUBSEP assoc[key] SSEP
  }
  return substr(acc, 1, length(acc) - 1)
}

function bindPosition(array,    acc) {
  for (i = 1; i <= length(array); i++) {
    acc = acc i SUBSEP array[i] SSEP
  }
  return substr(acc, 1, length(acc) - 1)
}

function flip(assoc) {
  for (idx in assoc) {
    assoc[assoc[idx]] = idx
  }
}

function printArray(arr, arrName) {
  for (idx in arr) {
    printf "%s[%s]: %s\n", (arrName ? arrName : "arr"), idx, arr[idx]
  }
}

function apply(arr, fn) {
  for (idx in arr) {
    arr[idx] = @fn(arr[idx])
  }
}

function strReprArr2dict(strReprArr, dict,    strRepr) {
  delete dict
  for (i = 1; i <= length(strReprArr); i++) {
    strRepr = strReprArr[i]
    partition(strRepr, SUBSEP, headtail)
    dict[headtail[1]] = headtail[2]
  }
}

function partition(str, sep, headtail) {
  headtail[1] = substr(str, 1, index(str, sep) - length(sep))
  headtail[2] = substr(str, index(str, sep) + length(sep))
}

function call(key, args,    bindings) {
  if (!BINDING[key]) {
    print "unregistered function call"
    return
  }
  buildParameter(key, args, params)

  switch (length(params)) {
    case 1:
      executeFunc1(fnname, params)
      break
    case 2:
      executeFunc2(fnname, params)
      break
    case 3:
      executeFunc3(fnname, params)
      break
    case 4:
      executeFunc4(fnname, params)
      break
    default:
      print "#(function params) must one of 1, 2, 3, or 4"
      return
  }

}

function buildParameter(key, args, params) {
  partition(BINDING[key], SSEP, fnBinding)
  fnname = fnBinding[1]
  inspectParams(fnname, paramDict)
  bindingRepr = fnBinding[2]
  bindingRepr2Dict(bindingRepr, bindingDict)

  for (idx in args) {
    params[idx] = args[idx]
  }
  for (idx in paramDict) {
    if (!params[paramDict[idx]]) {
      params[paramDict[idx]] = bindingDict[idx]
    }
  }
  if (length(paramDict) != length(params)) {
    print "invalid execution, mismatch between original function and binded function"
    delete params
  }
}

function bindingRepr2Dict(repr, bindingDict,    tmp) {
  split(repr, tmp, SSEP)
  for (idx in tmp) {
    split(tmp[idx], keyVal, SUBSEP)
    bindingDict[keyVal[1]] = keyVal[2]
  }
}

function inspectParams(fnname, paramDict) {
  posRepr = FUNCSIG[fnname]
  posRepr2Dict(posRepr, paramDict)
}

function posRepr2Dict(posRepr, paramDict,    tmp, posNameTuple) {
  split(posRepr, tmp, SSEP)
  for (idx in tmp) {
    split(tmp[idx], posName, SUBSEP)
    paramDict[posName[2]] = posName[1]
  }
}

function executeFunc1(fnname, params) {
  @fnname(params[1])
}

function executeFunc2(fnname, params) {
  @fnname(params[1], params[2])
}

function executeFunc3(fnname, params) {
  @fnname(params[1], params[2], params[3])
}

function executeFunc4(fnname, params) {
  @fnname(params[1], params[2], params[3], params[4])
}

# save function signature to global variable FUNCSIG
# argument information is of form list of tuples (position, parameter name)
# list element delemter: SSEP, key-value delemeter: SUBSEP
function saveSignature(fnname, argInfo) {
  posRepr = bindPosition(argInfo)
  FUNCSIG[fnname] = posRepr
}

function zip(arr1, arr2, dict) {
  minlen = (length(arr1) < length(arr2) ? length(arr1) : length(arr2))
  for (i = 1; i <= minlen; i++) {
    dict[arr1[i]] = arr2[i]
  }
}

BEGIN {
  SSEP = "\035"
  split("a b c", arginfo)
  saveSignature("sample", arginfo)
  split("b c", keys)
  split("5678 asdf", vals)
  zip(keys, vals, bindings)
  bind("partial", "sample", bindings)
  split("1", args)
  call("partial", args)
}

적절한 라이브러리에 분리하고 import를 사용하면 다음과 같다.


@include "common"
@include "functool"


function sample(a, b, c) {
  printf "a: %s, b: %s, c: %s\n", a, b, c
}

BEGIN {
  SSEP = "\035"
  split("a b c", arginfo)
  saveSignature("sample", arginfo)
  split("b c", keys)
  split("5678 asdf", vals)
  zip(keys, vals, bindings)
  bind("partial", "sample", bindings)
  split("1", args)
  call("partial", args)
}

출력 결과는 아래와 같다.

a: 1, b: 5678, c: asdf
@widehyo@hackers.pub

요새 awk에 대해 많은 관심을 가지고 있다. 특히 awk는 기본적으로 제공되는 feature가 가장 적은 언어중에 하나이기 때문에 다른 언어에서 편하게 사용했던 편의 기능을 직접 구현해서 사용해야 하는 경우가 많다. 하지만 awk 특유의 script스러움과 여러 편의기능 및 문법은 이에 익숙해진 사용자에게 빠져나가기 힘든 매력을 지니고 있기도 하다.

서론은 여기까지 하고 상당히 복잡한 구현이었던 functools.partial을 awk로 구현한 내용을 자세히 살펴보자.

먼저, awk에 대한 이해를 돕기 위해 언어가 가진 제약사항을 먼저 언급하고 가자.

  1. 모든 자료형은 string, number, or array(associative array)이다.
  2. 데이터를 표현하는 class나 struct를 제공하지 않는다.
  3. nested funciton을 지원하지 않는다.
  4. 따라서 closure를 지원하지 않는다.
  5. array를 return할 수 없다.
  6. multi return이 불가능하다.
  7. function pointer를 사용할 수 없다 (void *)를 이용한 함수 객체 활용이 불가하다.
  8. 변수는 기본적으로 global variable이다.
  9. (gawk가 아닌 awk 한정) multi dimensional array를 지원하지 않는다.
  10. 런타임에서 reflection이나 inspect를 할 수 있는 도구가 제공되지 않는다.

위의 제약사항 중 3, 4, 5, 6번은 C언어가 가진 제약사항을 고려하면 이해가 된다. 그러나 1번과 2번, 그리고 7번은 C언어 보다도 강력한 제약사항이라고 볼 수 있다. 그나마 7번은 gawk(GNU awk) 5.1 버전에서는 functionName = "myfunc"; @functionName을 이용하여 간접호출은 가능하다. 하지만 함수를 반환하거나, 변수에 함수를 할당하거나 파라미터에 함수를 넘기는 것은 불가능하다. 8번의 경우는 그나마 C언어의 함수 스코프가 지원되는 것을 이용하면 함수 내에서 지역변수 취급하고 싶은 변수를 parameter 자리에 넣음으로써 지역변수 취급이 가능하다.

그리고 이를 극복하기 위한 대응방안은 다음과 같다.

1번과 2번의 사용자정의 자료형 문제는 자료형을 표현할 수 있는 문자열을 설계(serialization과 같은 전략)하여 전달하다가 필요한 시점에 해당 문자열을 다시 원하는 형태로 복원하는 방법으로 극복할 수 있다. 이 전략은 posix awk가 multi dimensional array를 지원하지 않는 것에 대한 대응방안으로 a[i][j] 대신 a[i, j]로 사용한 점에서 착안했다. 편의를 위한 배열 인덱스 자리에 위치하는 i, j는 사실 "i\034j" 문자와 같다. 배열의 index에 위치하는 ,는 키보드로 입력 불가능한 문자인 "\034"로 치환되며, awk에서의 강력한 사용성을 지원하기 위해 내장변수 SUBSEP으로 \034 문자를 사용할 수 있다.

3번과 4번의 경우는 함수형 프로그래밍 방식을 선호하는 필자에게는 많은 불편함을 가져다 주었고, 이번 포스팅의 주된 도전과제였다. 간략히 극복전략을 이야기하자면 global table(associative array)를 이용하여 storage에 원하는 내용을 넣었다가 사용하는 방식으로 극복하였다.

5번과 6번은 생각보다 극복 난이도가 낮았는데, C언어의 call by reference 방식으로 우회하면 된다. C언어에서 array를 sort하기 위해 배열의 포인터를 함수에 넘기고 안에서 swap한 것과 근본적으로 같은 방식이다. 한편, multi return을 위해 새로운 배열을 만들어 return하고 싶을 때는 해당 배열을 parameter로 넘기고 함수 본문 시작시 delete 문을 이용해 우연히 같은 이름을 사용하는 global variable 문제를 방지하였다. 그리고 이 방법은 awk의 기본 함수인 split("text", "array", "separator")에서 자연스럽게 사용하는 방법이기 때문에 awk의 idiom과도 부합한다.

7번은 gawk의 힘을 빌려 @fn 을 이용한다. man awk를 읽다가 @fn을 보고 이거 잘하면 함수형 프로그래밍 할 수 있겠는데 라는 생각이 이 글의 근본적인 모티베이션이 되었다. 하지만 10번의 제약사항에 의하여 런타임에서 dynamic하게 함수/매개변수에 접근할 수 없었기 때문에 debugger가 참조하는 프레임 객체와 비슷한 것 중 functools.partial에서 필요한 정보만 담은 global variable을 직접 만들어 구현했다.

가장 먼저 진행한 부분은 함수 signature를 저장하는 방법을 찾는 것이었다. functools.partial과 같은 함수는 parameter의 위치 및 key를 이용하여 default parameter를 binding할 수 있었기 때문에 해당하는 정보를 저장해야 했다. 결론만 말하자면 다음과 같은 함수가 있을 때

function sample(a, b, c) { ... }

[(a, 1), (b, 2), (c, 3)] 과 같은 형태로 함수의 signature가 저장되기를 원했다. 하지만 array는 element로 array를 가질 수 없기에(array의 원소에 array를 할당하려 하면 syntax error), [(a, 1), (b, 2), (c, 3)]의 string representation을 다음과 같이 저장했다.

SSEP = "\035"
"1" SUBSEP "a" SSEP "2" SUBSEP "b" SSEP "3" SUBSEP "c"

구분자가 2개가 필요했는데 하나는 튜플 안에서 데이터를 구분하는 구분자로 사용된 SUBSEP, 그리고 리스트에서 각 튜플의 구분자로 사용된 SSEP이다. 이렇게 function signature를 string으로 표현하여 global variable인 FUNCSIG 테이블에 함수명을 키로, function signature의 string repr을 value로 저장한다. 그러면 대충 python으로 보면 이런 느낌이 된다.

FUNCSIG["sample"] = "1\034a\0352\034b\0353\034c"

그리고 실제로 사용할 때는 SSEP과 SUBSEP으로 split하여(deserialize하여) {"a":1,"b":2,"c":3} 과 같은 형태로 사용한다.

# save function signature to global variable FUNCSIG
# argument information is of form list of tuples (position, parameter name)
# list element delemter: SSEP, key-value delemeter: SUBSEP
function saveSignature(fnname, argInfo) {
  posRepr = bindPosition(argInfo)
  FUNCSIG[fnname] = posRepr
}

function bindPosition(array,    acc) {
  for (i = 1; i <= length(array); i++) {
    acc = acc i SUBSEP array[i] SSEP
  }
  return substr(acc, 1, length(acc) - 1)
}

두번째로는 진행한 함수는 binding된 함수를 만드는 함수이다. functools의 partial이 하는 역할과 같다고 보면 된다.

원본함수명(fnname)과 default parameter의 정보(bindings)를 받아 원본 함수의 parameter 중 명시적으로 호출하지 않은 매개변수는 bindings로 미리 정의한 매개변수를 사용하는 함수의 이름을 key로 등록하는 동작을 awk용으로 바꾼 것이다.

>>> from functools import partial
>>> def fnname(a, b):
...     print(f"a: {a}, b: {b}")
... 
>>> key = partial(fnname, b=3)
>>> key(1)
a: 1, b: 3
function fnname(a, b) {
    printf "a: %s, b: %s", a, b
}
# 정의부
key = "partial_fn"
bind(key, "fnname", "b" SUBSEP "3") # partial_fn = partial(fnname, b=3)과 같은 역할

# 사용부
call(key, "a" SUBSEP "1") # partial_fn(a=1)과 같은 역할 == @key(a=1)과 같은 역할

매개변수는 argument를 binding하는 정보를 담고 있는 array(bindings)와 원본 함수명(fnname) 그리고 binding한 함수명(key)으로 구성된다.

함수명을 통한 함수 간접호출은 gawk에서 @fn 문법으로 가능하기 때문에, 어떤 함수에 어떤 parameter가 바인딩 되었는지를 정보로 전달하고 binding된 새로운 함수를 key로 global variable BINDING이 가지고 있다가 call 함수에 의해 해당 정보를 찾아 reconstruction을 통해 호출하는 방법이다. 최대한의 유연성을 확보하기 위하여 bindings는 associative array로 설계했고, 위에서 저장한 function signature와 실제 실행 책임을 가진 함수 call에서 호출할 args와 함께 사용된다.

binding한 정보는 [원본함수명] [(매개변수1, 값1), (매개변수2, 값2), ...] 와 같은 형태로 저장된다. 물론, awk에는 string과 number만 primitive type으로 가지므로 string representation 형태로 저장한다.

# save binding arguments to global variable BINDING
# key will be used as partial function(default parameter from binding info)
# the form value consists two parts, head and tails separated with first SSEP
# head contains `original function name`
# tail is of form string representation of the list of tuple, which argName SUBSEP value
# each elements is joined with SSEP
function bind(key, fnname, bindings,    argRepr) {
  argRepr = fnname SSEP

  if (length(bindings) == 0) {
    BINDING[key] = argRepr
    return
  }
  argRepr = argRepr serializeAssoc(bindings)
  BINDING[key] = argRepr
}

헬퍼 함수 serializeAssoc을 사용했다.

function serializeAssoc(assoc,    acc) {
  for (key in assoc) {
    acc = acc key SUBSEP assoc[key] SSEP
  }
  return substr(acc, 1, length(acc) - 1)
}

위에서 만든 바인딩된 함수를 호출하는 함수 call이다. 실제 함수호출을 담당하고 내부로직이 가장 복잡한 함수이다. function signature는 다음과 같다.

function call(key, args,    bindings)

위에서 정의한 key로 BINDING에서 argRepr을 가져온다. argRepr의 첫 번째 위치에 있는 함수명을 추출한다 추출한 함수명으로 FUNCSIG에 저장된 posRepr를 가져온다.

argRepr의 두 번째 위치에 있는 binding 정보를 복원하고 원본함수의 function signature를 담고 있는 posRepr을 각각 복원하여, 실제로 호출할 args와 결합하여 호출한다.

전반부는 실제 파라미터를 생성하는 부분으로 구성된다.

function call(key, args,    bindings) {
  if (!BINDING[key]) {
    print "unregistered function call"
    return
  }
  buildParameter(key, args, params)

buildParameter 함수는 바인딩된 함수를 호출할 args를 우선적으로 적용하고 args에서 채워지지 않은 매개변수는 bindingRepr에서 표현된 매개변수 정보에서 채워넣는다. 만약 모든 parameter가 채워지지 않았다면 에러를 출력한다. multi return을 위하여 params 배열을 이용한다.

function buildParameter(key, args, params) {
  partition(BINDING[key], SSEP, fnBinding)
  fnname = fnBinding[1]
  inspectParams(fnname, paramDict)
  bindingRepr = fnBinding[2]
  bindingRepr2Dict(bindingRepr, bindingDict)

  for (idx in args) {
    params[idx] = args[idx]
  }
  for (idx in paramDict) {
    if (!params[paramDict[idx]]) {
      params[paramDict[idx]] = bindingDict[idx]
    }
  }
  if (length(paramDict) != length(params)) {
    print "invalid execution, mismatch between original function and binded function"
    delete params
  }
}

partition은 헬퍼함수이다. separator를 기준으로 전후를 잘라낸다. 역시 multi return이 필요하기 때문에 headtail 매개변수 배열을 이용한다.

function partition(str, sep, headtail) {
  headtail[1] = substr(str, 1, index(str, sep) - length(sep))
  headtail[2] = substr(str, index(str, sep) + length(sep))
}

bindingRepr2Dict는 string representation을 dictionary(associative array)로 복원하는 함수이다. 내용이 간단하므로 먼저 소개한다. BINDING global 변수와 관련있다.

function bindingRepr2Dict(repr, bindingDict,    tmp) {
  split(repr, tmp, SSEP)
  for (idx in tmp) {
    split(tmp[idx], keyVal, SUBSEP)
    bindingDict[keyVal[1]] = keyVal[2]
  }
}

inspectParams는 원본 함수의 function signature를 담고 있는 FUNCSIG global 변수와 관련있다.

function inspectParams(fnname, paramDict) {
  posRepr = FUNCSIG[fnname]
  posRepr2Dict(posRepr, paramDict)
}

function posRepr2Dict(posRepr, paramDict,    tmp, posNameTuple) {
  split(posRepr, tmp, SSEP)
  for (idx in tmp) {
    split(tmp[idx], posName, SUBSEP)
    paramDict[posName[2]] = posName[1]
  }
}

아래 부분이 비즈니스 로직으로, 명시적으로 호출하는 매개변수를 이용해 원본 함수의 funciton signature를 최대한 채워 넣은 후 비어있는 매개변수에 대하여 bindingDict에서 정의되어 있다면 나머지를 채워 넣는 식으로 작동한다.

  for (idx in args) {
    params[idx] = args[idx]
  }
  for (idx in paramDict) {
    if (!params[paramDict[idx]]) {
      params[paramDict[idx]] = bindingDict[idx]
    }
  }

물론, 명시적으로 주어진 parameter와 binding된 매개변수 모두 사용해도 함수가 요구하는 매개변수를 모두 채우지 못한다면 에러메시지를 출력한다.

  if (length(paramDict) != length(params)) {
    print "invalid execution, mismatch between original function and binded function"
    delete params
  }

사실 위의 코드에서는 exit로 나가도 좋다

이렇게 parameter를 구성하면 남은 작업은 간단하다. 매개변수의 개수에 따라 해당하는 함수를 동적으로 호출하는 부분만 남았다. call 함수의 후반부 코드를 보자.


  switch (length(params)) {
    case 1:
      executeFunc1(fnname, params)
      break
    case 2:
      executeFunc2(fnname, params)
      break
    case 3:
      executeFunc3(fnname, params)
      break
    case 4:
      executeFunc4(fnname, params)
      break
    default:
      print "#(function params) must one of 1, 2, 3, or 4"
      return
  }

여러 helper 함수를 사용하는데, 이것은 awk가 동적 프로그래밍이 불가능하기 때문이다. 쉽게 말하면 eval이 없어서 각종 템플릿에 맞는 함수를 하나씩 사전에 정의해 두어야 한다.


function executeFunc1(fnname, params) {
  @fnname(params[1])
}

function executeFunc2(fnname, params) {
  @fnname(params[1], params[2])
}

function executeFunc3(fnname, params) {
  @fnname(params[1], params[2], params[3])
}

function executeFunc4(fnname, params) {
  @fnname(params[1], params[2], params[3], params[4])
}

이상을 모두 적용한 전체 코드는 다음과 같다.


function sample(a, b, c) {
  printf "a: %s, b: %s, c: %s\n", a, b, c
}

# save binding arguments to global variable BINDING
# key will be used as partial function(default parameter from binding info)
# the form value consists two parts, head and tails separated with first SSEP
# head contains `original function name`
# tail is of form string representation of the list of tuple, which argName SUBSEP value
# each elements is joined with SSEP
function bind(key, fnname, bindings,    argRepr) {
  argRepr = fnname SSEP

  if (length(bindings) == 0) {
    BINDING[key] = argRepr
    return
  }
  argRepr = argRepr serializeAssoc(bindings)
  BINDING[key] = argRepr
}

function serializeAssoc(assoc,    acc) {
  for (key in assoc) {
    acc = acc key SUBSEP assoc[key] SSEP
  }
  return substr(acc, 1, length(acc) - 1)
}

function bindPosition(array,    acc) {
  for (i = 1; i <= length(array); i++) {
    acc = acc i SUBSEP array[i] SSEP
  }
  return substr(acc, 1, length(acc) - 1)
}

function flip(assoc) {
  for (idx in assoc) {
    assoc[assoc[idx]] = idx
  }
}

function printArray(arr, arrName) {
  for (idx in arr) {
    printf "%s[%s]: %s\n", (arrName ? arrName : "arr"), idx, arr[idx]
  }
}

function apply(arr, fn) {
  for (idx in arr) {
    arr[idx] = @fn(arr[idx])
  }
}

function strReprArr2dict(strReprArr, dict,    strRepr) {
  delete dict
  for (i = 1; i <= length(strReprArr); i++) {
    strRepr = strReprArr[i]
    partition(strRepr, SUBSEP, headtail)
    dict[headtail[1]] = headtail[2]
  }
}

function partition(str, sep, headtail) {
  headtail[1] = substr(str, 1, index(str, sep) - length(sep))
  headtail[2] = substr(str, index(str, sep) + length(sep))
}

function call(key, args,    bindings) {
  if (!BINDING[key]) {
    print "unregistered function call"
    return
  }
  buildParameter(key, args, params)

  switch (length(params)) {
    case 1:
      executeFunc1(fnname, params)
      break
    case 2:
      executeFunc2(fnname, params)
      break
    case 3:
      executeFunc3(fnname, params)
      break
    case 4:
      executeFunc4(fnname, params)
      break
    default:
      print "#(function params) must one of 1, 2, 3, or 4"
      return
  }

}

function buildParameter(key, args, params) {
  partition(BINDING[key], SSEP, fnBinding)
  fnname = fnBinding[1]
  inspectParams(fnname, paramDict)
  bindingRepr = fnBinding[2]
  bindingRepr2Dict(bindingRepr, bindingDict)

  for (idx in args) {
    params[idx] = args[idx]
  }
  for (idx in paramDict) {
    if (!params[paramDict[idx]]) {
      params[paramDict[idx]] = bindingDict[idx]
    }
  }
  if (length(paramDict) != length(params)) {
    print "invalid execution, mismatch between original function and binded function"
    delete params
  }
}

function bindingRepr2Dict(repr, bindingDict,    tmp) {
  split(repr, tmp, SSEP)
  for (idx in tmp) {
    split(tmp[idx], keyVal, SUBSEP)
    bindingDict[keyVal[1]] = keyVal[2]
  }
}

function inspectParams(fnname, paramDict) {
  posRepr = FUNCSIG[fnname]
  posRepr2Dict(posRepr, paramDict)
}

function posRepr2Dict(posRepr, paramDict,    tmp, posNameTuple) {
  split(posRepr, tmp, SSEP)
  for (idx in tmp) {
    split(tmp[idx], posName, SUBSEP)
    paramDict[posName[2]] = posName[1]
  }
}

function executeFunc1(fnname, params) {
  @fnname(params[1])
}

function executeFunc2(fnname, params) {
  @fnname(params[1], params[2])
}

function executeFunc3(fnname, params) {
  @fnname(params[1], params[2], params[3])
}

function executeFunc4(fnname, params) {
  @fnname(params[1], params[2], params[3], params[4])
}

# save function signature to global variable FUNCSIG
# argument information is of form list of tuples (position, parameter name)
# list element delemter: SSEP, key-value delemeter: SUBSEP
function saveSignature(fnname, argInfo) {
  posRepr = bindPosition(argInfo)
  FUNCSIG[fnname] = posRepr
}

function zip(arr1, arr2, dict) {
  minlen = (length(arr1) < length(arr2) ? length(arr1) : length(arr2))
  for (i = 1; i <= minlen; i++) {
    dict[arr1[i]] = arr2[i]
  }
}

BEGIN {
  SSEP = "\035"
  split("a b c", arginfo)
  saveSignature("sample", arginfo)
  split("b c", keys)
  split("5678 asdf", vals)
  zip(keys, vals, bindings)
  bind("partial", "sample", bindings)
  split("1", args)
  call("partial", args)
}

적절한 라이브러리에 분리하고 import를 사용하면 다음과 같다.


@include "common"
@include "functool"


function sample(a, b, c) {
  printf "a: %s, b: %s, c: %s\n", a, b, c
}

BEGIN {
  SSEP = "\035"
  split("a b c", arginfo)
  saveSignature("sample", arginfo)
  split("b c", keys)
  split("5678 asdf", vals)
  zip(keys, vals, bindings)
  bind("partial", "sample", bindings)
  split("1", args)
  call("partial", args)
}

출력 결과는 아래와 같다.

a: 1, b: 5678, c: asdf
@hugovk@mastodon.social · Reply to Hugo van Kemenade

Just released! 🚀

em-keyboard 5.3.0

🎲 Pick a random emoji from a search. For example:

❯ em --search music --random
Copied! 👩‍🎤 woman_singer

🧛‍♂️ Drop support for Python 3.9

github.com/hugovk/em-keyboard/

Release v5.3.0 · hugovk/em-keyboard

Added Pick a random emoji from a search (#184) @hugovk Changed Drop support for Python 3.9 (#185) @hugovk

@toxi@mastodon.thi.ng

— New version 3.1.0 of the recently talked about thi.ng/args package, a declarative & functional CLI argument parser & app framework. I updated the arg specifications to be fully self-describing & serializable (with minor exceptions), and streamlined the API for factory functions to define the specs.

Why is this useful? For example, now I can (already have!) implemented a CLI as separate short-lived client/process which only acts as RPC frontend/proxy for the actual CLI commands defined & executed in a long running app server, which is heavily based on a plugin architecture. Each plugin can contribute any number of CLI commands, each with its own set of args/options... When the CLI client app is launched, it first retrieves a list of these registered commands and all their options from the server, then uses the thi.ng/args CLI framework to select the right command, validate its options or display formatted usage info. If all is ok, the command is then triggered via an HTTP request to the app server, executes there and the command's log messages are send back as response...

Block diagram giving a highlevel overview of a software architecture consisting of: app server, router, CLI and a number of plugins. These app component interact with each other via registrations and delegations. A separate box "CLI RPC" relates to a separate client app which only interacts with the app server and is used as remote frontend for invoking commands inside the (much longer running) main app.
ALT text

Block diagram giving a highlevel overview of a software architecture consisting of: app server, router, CLI and a number of plugins. These app component interact with each other via registrations and delegations. A separate box "CLI RPC" relates to a separate client app which only interacts with the app server and is used as remote frontend for invoking commands inside the (much longer running) main app.

@toxi@mastodon.thi.ng

Finally got around documenting a little more the small CLI app "framework" I've been using for almost a dozen projects now (incl. several work projects). The package in question is now already 3 years old (thi.ng/args), but I've only just managed now to add a basic, commented usage example for this `cliApp()` feature to the readme:

Defining a multi-command CLI app (incl. two sub-commands):
github.com/thi-ng/umbrella/blo

Also part of this: I've refactored a few other projects to simplify their CLI handling using this `cliApp()` wrapper (project links are in the above readme, in case you'd like to see more advanced/realworld uses...) One of the (non-public) work projects ended up consisting of up to a dozen sub-commands and I found this declarative and modular setup to be very, very helpful (and elegant)...

github.com

umbrella/packages/args/README.md at develop · thi-ng/umbrella

⛱ Broadly scoped ecosystem & mono-repository of 210 TypeScript projects (and ~185 examples) for general purpose, functional, data driven development - thi-ng/umbrella

@fp@social.lol

If you're working in the command line daily, here are some tools that made my life better:

- github.com/ajeetdsouza/zoxide for switching directories with more comfort
- github.com/eza-community/eza for more pleasant directory listings
- atuin.sh for helpful history
- github.com/BurntSushi/ripgrep for faster search in files
- github.com/junegunn/fzf for finding files faster
- starship.rs for fast helpful shell prompts

And give fishshell.com a try while you're at it, it's great.

fishshell.com

fish shell

A smart and user-friendly command line shell

@fp@social.lol

If you're working in the command line daily, here are some tools that made my life better:

- github.com/ajeetdsouza/zoxide for switching directories with more comfort
- github.com/eza-community/eza for more pleasant directory listings
- atuin.sh for helpful history
- github.com/BurntSushi/ripgrep for faster search in files
- github.com/junegunn/fzf for finding files faster
- starship.rs for fast helpful shell prompts

And give fishshell.com a try while you're at it, it's great.

fishshell.com

fish shell

A smart and user-friendly command line shell

@nev@status.nevillepark.ca

I only just learned you can use the mouse in the TTY! (The Linux text-only console you get to by pressing Ctrl+Alt+F1, F2, etc.)

Some applications that enable this are gpm and consolation.

consolation worked better for me, especially when in tmux; however, I still can't get scrolling with middle button and trackpoint to work, which is what would be the most useful for me. I don't know if it has to do with consolation, tmux mouse bindings, my particular ThinkPad model, etc. I can't find anything online about this, so if you've also dabbled in this, please share your experience!

#CommandLine #cli #tmux #tty #linux #thinkpad

salsa.debian.org

Consolation Team / consolation · GitLab

This package provides copy-paste and scrolling support to the Linux console. It is based on the libinput library and supports all pointer devices and settings provided by this...

@toxi@mastodon.thi.ng

Speaking of new tools: Here's some other open-ended and work-in-progress tooling I published recently:

Assorted CLI utilities for data wrangling & media conversion
codeberg.org/thi.ng/thing-tools

This is (will be) a Swiss-army knife type multi-tool for frequent little tasks I've been encountering and not found satisfactory equivalent other solutions for. So far, there're only two commands published, but a dozen or so more are to come (for which I still have more cleaning up to do, also in upstream projects):

- CSV-to-JSON parsing/conversion, with configurable filtering, renaming and column value coercions
- De-dupe lines with support for regexp-based inclusions, exclusions and pattern-based uniqueness. Patterns can be read from files.

The readme contains installation instructions and documentation for all commands, their options and some example use cases. E.g. I regularly use the `dedupe-lines` command to cleanup my `.bash_history` file.

Using bun.sh, the tool can be compiled into a standalone executable.

All commands share common infrastructure of the main CLI tooling (based on thi.ng/args and many other thi.ng/umbrella packages). This reduces the code size of each command and makes it trivial to add additional commands (or share functionality, invoke some of those other commands for sub-tasks). There're also re-usable CLI arg specs to provide a uniform "API" for certain types of parameters.

Some of the still unreleased commands are for `ffmpeg` workflows/tasks, will update when ready...

thi.ng

thi.ng/umbrella

@hongminhee@hollo.social

Optique 0.4.0 Released!

Big update for our type-safe combinatorial parser for :

  • Labeled merge groups: organize options logically
  • Rich docs: brief, description & footer support
  • @optique/temporal: new package for date/time parsing
  • showDefault: automatic default value display

The help text has never looked this good!

.js

hackers.pub

Optique 0.4.0: Better help, rich docs, and Temporal support

Optique 0.4.0 introduces enhancements to streamline CLI development in TypeScript. This release focuses on improving help text organization through labeled merge groups and a new `group()` combinator, making complex CLIs more user-friendly by organizing options under clear sections. Comprehensive documentation support is added via the `run()` function, allowing brief descriptions, detailed explanations, and footers without altering parser definitions. The update also includes Temporal API support with the `@optique/temporal` package, enabling type-safe parsing for dates, times, and time zones. Improved type inference for `merge()` and `tuple()` combinators enhances type safety, alongside minor breaking changes. These updates aim to make CLI construction more intuitive and maintainable, offering developers greater control over user experience and code structure.

@hongminhee@hackers.pub

We're excited to announce Optique 0.4.0, which brings significant improvements to help text organization, enhanced documentation capabilities, and introduces comprehensive Temporal API support.

Optique is a type-safe combinatorial CLI parser for TypeScript that makes building command-line interfaces intuitive and maintainable. This release focuses on making your CLI applications more user-friendly and maintainable.

Better help text organization

One of the most visible improvements in Optique 0.4.0 is the enhanced help text organization. You can now label and group your options more effectively, making complex CLIs much more approachable for users.

Labeled merge groups

The merge() combinator now accepts an optional label parameter, solving a common pain point where developers had to choose between clean code structure and organized help output:

// Before: unlabeled merged options appeared scattered
const config = merge(connectionOptions, performanceOptions);

// Now: group related options under a clear section
const config = merge(
  "Server Configuration",  // New label parameter
  connectionOptions,
  performanceOptions
);

This simple addition makes a huge difference in help text readability, especially for CLIs with many options spread across multiple reusable modules.

The resulting help output clearly organizes options under the Server Configuration section:

Demo app showcasing labeled merge groups
Usage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries
       INTEGER

Server Configuration:
  --host STRING               Server hostname or IP address
  --port INTEGER              Port number for the connection
  --timeout INTEGER           Connection timeout in seconds
  --retries INTEGER           Number of retry attempts

The new group() combinator

For cases where merge() doesn't apply, the new group() combinator lets you wrap any parser with a documentation label:

// Group mutually exclusive options under a clear section
const outputFormat = group(
  "Output Format",
  or(
    map(flag("--json"), () => "json"),
    map(flag("--yaml"), () => "yaml"),
    map(flag("--xml"), () => "xml"),
  )
);

This is particularly useful for organizing mutually exclusive flags, multiple inputs, or any parser that doesn't natively support labeling. The resulting help text becomes much more scannable and user-friendly.

Here's how the grouped output format options appear in the help text:

Demo app showcasing group combinator
Usage: demo-group.ts --json
       demo-group.ts --yaml
       demo-group.ts --xml

Output Format:
  --json                      Output in JSON format
  --yaml                      Output in YAML format
  --xml                       Output in XML format

Rich documentation support

Optique 0.4.0 introduces comprehensive documentation fields that can be added directly through the run() function, eliminating the need to modify parser definitions for documentation purposes.

Brief descriptions, detailed explanations, and footers

Both @optique/core/facade and @optique/run now support brief, description, and footer options through the run() function:

import { run } from "@optique/run";
import { message } from "@optique/core/message";

const result = run(parser, {
  brief: message`A powerful data processing tool`,
  description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`,
  footer: message`Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs`,
  help: "option"
});

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

The complete help output demonstrates the rich documentation features with brief description, detailed explanation, option descriptions, default values, and footer information:

A powerful data processing tool
Usage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRING

This tool provides comprehensive data processing capabilities with support for
multiple formats and transformations. It can handle JSON, YAML, and CSV files
with automatic format detection.

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]
  --verbose STRING            Verbosity level

Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

Display default values

A frequently requested feature is now available: showing default values directly in help text. Enable this with the new showDefault option when using withDefault():

const parser = object({
  port: withDefault(
    option("--port", integer(), { description: message`Server port number` }),
    3000,
  ),
  format: withDefault(
    option("--format", string(), { description: message`Output format` }),
    "json",
  ),
});

run(parser, { showDefault: true });

// Or with custom formatting:
run(parser, {
  showDefault: {
    prefix: " (default: ",
    suffix: ")"
  }  // Shows: --port (default: 3000)
});

Default values are automatically dimmed when colors are enabled, making them visually distinct while remaining readable.

The help output shows default values clearly marked next to each option:

Usage: demo-defaults.ts [--port INTEGER] [--format STRING]

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]

Temporal API support

Optique 0.4.0 introduces a new package, @optique/temporal, providing comprehensive support for the modern Temporal API. This brings type-safe parsing for dates, times, durations, and time zones:

import { instant, duration, zonedDateTime } from "@optique/temporal";
import { option } from "@optique/core/parser";

const parser = object({
  // Parse ISO 8601 timestamps
  timestamp: option("--at", instant()),

  // Parse durations like "PT30M" or "P1DT2H"
  timeout: option("--timeout", duration()),

  // Parse zoned datetime with timezone info
  meeting: option("--meeting", zonedDateTime()),
});

The temporal parsers return native Temporal objects with full functionality:

const result = parse(timestampArg, ["2023-12-25T10:30:00Z"]);
if (result.success) {
  const instant = result.value;
  console.log(`UTC: ${instant.toString()}`);
  console.log(`Seoul: ${instant.toZonedDateTimeISO("Asia/Seoul")}`);
}

Install the new package with:

npm add @optique/temporal

Improved type inference

The merge() combinator now supports up to 10 parsers (previously 5), and the tuple() parser has improved type inference using TypeScript's const type parameter. These enhancements enable more complex CLI structures while maintaining perfect type safety.

Breaking changes

While we've maintained backward compatibility for most APIs, there are a few changes to be aware of:

  • The Parser.getDocFragments() method now uses DocState<TState> instead of direct state values (only affects custom parser implementations)
  • The merge() combinator now enforces stricter type constraints at compile time, rejecting non-object-producing parsers

Learn more

For a complete list of changes, bug fixes, and improvements, see the full changelog.

Check out the updated documentation:

Installation

Upgrade to Optique 0.4.0:

npm update @optique/core @optique/run
# or
deno add jsr:@optique/core@^0.4.0 jsr:@optique/run@^0.4.0

Add temporal support (optional):

npm add @optique/temporal
# or
deno add jsr:@optique/temporal

We hope these improvements make building CLI applications with Optique even more enjoyable. As always, we welcome your feedback and contributions on GitHub.

github.com

GitHub - dahlia/optique: Type-safe combinatorial CLI parser for TypeScript

Type-safe combinatorial CLI parser for TypeScript. Contribute to dahlia/optique development by creating an account on GitHub.

@hongminhee@hollo.social

Optique 0.4.0 Released!

Big update for our type-safe combinatorial parser for :

  • Labeled merge groups: organize options logically
  • Rich docs: brief, description & footer support
  • @optique/temporal: new package for date/time parsing
  • showDefault: automatic default value display

The help text has never looked this good!

.js

hackers.pub

Optique 0.4.0: Better help, rich docs, and Temporal support

Optique 0.4.0 introduces enhancements to streamline CLI development in TypeScript. This release focuses on improving help text organization through labeled merge groups and a new `group()` combinator, making complex CLIs more user-friendly by organizing options under clear sections. Comprehensive documentation support is added via the `run()` function, allowing brief descriptions, detailed explanations, and footers without altering parser definitions. The update also includes Temporal API support with the `@optique/temporal` package, enabling type-safe parsing for dates, times, and time zones. Improved type inference for `merge()` and `tuple()` combinators enhances type safety, alongside minor breaking changes. These updates aim to make CLI construction more intuitive and maintainable, offering developers greater control over user experience and code structure.

@hongminhee@hackers.pub

We're excited to announce Optique 0.4.0, which brings significant improvements to help text organization, enhanced documentation capabilities, and introduces comprehensive Temporal API support.

Optique is a type-safe combinatorial CLI parser for TypeScript that makes building command-line interfaces intuitive and maintainable. This release focuses on making your CLI applications more user-friendly and maintainable.

Better help text organization

One of the most visible improvements in Optique 0.4.0 is the enhanced help text organization. You can now label and group your options more effectively, making complex CLIs much more approachable for users.

Labeled merge groups

The merge() combinator now accepts an optional label parameter, solving a common pain point where developers had to choose between clean code structure and organized help output:

// Before: unlabeled merged options appeared scattered
const config = merge(connectionOptions, performanceOptions);

// Now: group related options under a clear section
const config = merge(
  "Server Configuration",  // New label parameter
  connectionOptions,
  performanceOptions
);

This simple addition makes a huge difference in help text readability, especially for CLIs with many options spread across multiple reusable modules.

The resulting help output clearly organizes options under the Server Configuration section:

Demo app showcasing labeled merge groups
Usage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries
       INTEGER

Server Configuration:
  --host STRING               Server hostname or IP address
  --port INTEGER              Port number for the connection
  --timeout INTEGER           Connection timeout in seconds
  --retries INTEGER           Number of retry attempts

The new group() combinator

For cases where merge() doesn't apply, the new group() combinator lets you wrap any parser with a documentation label:

// Group mutually exclusive options under a clear section
const outputFormat = group(
  "Output Format",
  or(
    map(flag("--json"), () => "json"),
    map(flag("--yaml"), () => "yaml"),
    map(flag("--xml"), () => "xml"),
  )
);

This is particularly useful for organizing mutually exclusive flags, multiple inputs, or any parser that doesn't natively support labeling. The resulting help text becomes much more scannable and user-friendly.

Here's how the grouped output format options appear in the help text:

Demo app showcasing group combinator
Usage: demo-group.ts --json
       demo-group.ts --yaml
       demo-group.ts --xml

Output Format:
  --json                      Output in JSON format
  --yaml                      Output in YAML format
  --xml                       Output in XML format

Rich documentation support

Optique 0.4.0 introduces comprehensive documentation fields that can be added directly through the run() function, eliminating the need to modify parser definitions for documentation purposes.

Brief descriptions, detailed explanations, and footers

Both @optique/core/facade and @optique/run now support brief, description, and footer options through the run() function:

import { run } from "@optique/run";
import { message } from "@optique/core/message";

const result = run(parser, {
  brief: message`A powerful data processing tool`,
  description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`,
  footer: message`Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs`,
  help: "option"
});

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

The complete help output demonstrates the rich documentation features with brief description, detailed explanation, option descriptions, default values, and footer information:

A powerful data processing tool
Usage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRING

This tool provides comprehensive data processing capabilities with support for
multiple formats and transformations. It can handle JSON, YAML, and CSV files
with automatic format detection.

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]
  --verbose STRING            Verbosity level

Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

Display default values

A frequently requested feature is now available: showing default values directly in help text. Enable this with the new showDefault option when using withDefault():

const parser = object({
  port: withDefault(
    option("--port", integer(), { description: message`Server port number` }),
    3000,
  ),
  format: withDefault(
    option("--format", string(), { description: message`Output format` }),
    "json",
  ),
});

run(parser, { showDefault: true });

// Or with custom formatting:
run(parser, {
  showDefault: {
    prefix: " (default: ",
    suffix: ")"
  }  // Shows: --port (default: 3000)
});

Default values are automatically dimmed when colors are enabled, making them visually distinct while remaining readable.

The help output shows default values clearly marked next to each option:

Usage: demo-defaults.ts [--port INTEGER] [--format STRING]

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]

Temporal API support

Optique 0.4.0 introduces a new package, @optique/temporal, providing comprehensive support for the modern Temporal API. This brings type-safe parsing for dates, times, durations, and time zones:

import { instant, duration, zonedDateTime } from "@optique/temporal";
import { option } from "@optique/core/parser";

const parser = object({
  // Parse ISO 8601 timestamps
  timestamp: option("--at", instant()),

  // Parse durations like "PT30M" or "P1DT2H"
  timeout: option("--timeout", duration()),

  // Parse zoned datetime with timezone info
  meeting: option("--meeting", zonedDateTime()),
});

The temporal parsers return native Temporal objects with full functionality:

const result = parse(timestampArg, ["2023-12-25T10:30:00Z"]);
if (result.success) {
  const instant = result.value;
  console.log(`UTC: ${instant.toString()}`);
  console.log(`Seoul: ${instant.toZonedDateTimeISO("Asia/Seoul")}`);
}

Install the new package with:

npm add @optique/temporal

Improved type inference

The merge() combinator now supports up to 10 parsers (previously 5), and the tuple() parser has improved type inference using TypeScript's const type parameter. These enhancements enable more complex CLI structures while maintaining perfect type safety.

Breaking changes

While we've maintained backward compatibility for most APIs, there are a few changes to be aware of:

  • The Parser.getDocFragments() method now uses DocState<TState> instead of direct state values (only affects custom parser implementations)
  • The merge() combinator now enforces stricter type constraints at compile time, rejecting non-object-producing parsers

Learn more

For a complete list of changes, bug fixes, and improvements, see the full changelog.

Check out the updated documentation:

Installation

Upgrade to Optique 0.4.0:

npm update @optique/core @optique/run
# or
deno add jsr:@optique/core@^0.4.0 jsr:@optique/run@^0.4.0

Add temporal support (optional):

npm add @optique/temporal
# or
deno add jsr:@optique/temporal

We hope these improvements make building CLI applications with Optique even more enjoyable. As always, we welcome your feedback and contributions on GitHub.

github.com

GitHub - dahlia/optique: Type-safe combinatorial CLI parser for TypeScript

Type-safe combinatorial CLI parser for TypeScript. Contribute to dahlia/optique development by creating an account on GitHub.

@hongminhee@hollo.social

Optique 0.4.0 Released!

Big update for our type-safe combinatorial parser for :

  • Labeled merge groups: organize options logically
  • Rich docs: brief, description & footer support
  • @optique/temporal: new package for date/time parsing
  • showDefault: automatic default value display

The help text has never looked this good!

.js

hackers.pub

Optique 0.4.0: Better help, rich docs, and Temporal support

Optique 0.4.0 introduces enhancements to streamline CLI development in TypeScript. This release focuses on improving help text organization through labeled merge groups and a new `group()` combinator, making complex CLIs more user-friendly by organizing options under clear sections. Comprehensive documentation support is added via the `run()` function, allowing brief descriptions, detailed explanations, and footers without altering parser definitions. The update also includes Temporal API support with the `@optique/temporal` package, enabling type-safe parsing for dates, times, and time zones. Improved type inference for `merge()` and `tuple()` combinators enhances type safety, alongside minor breaking changes. These updates aim to make CLI construction more intuitive and maintainable, offering developers greater control over user experience and code structure.

@hongminhee@hackers.pub

We're excited to announce Optique 0.4.0, which brings significant improvements to help text organization, enhanced documentation capabilities, and introduces comprehensive Temporal API support.

Optique is a type-safe combinatorial CLI parser for TypeScript that makes building command-line interfaces intuitive and maintainable. This release focuses on making your CLI applications more user-friendly and maintainable.

Better help text organization

One of the most visible improvements in Optique 0.4.0 is the enhanced help text organization. You can now label and group your options more effectively, making complex CLIs much more approachable for users.

Labeled merge groups

The merge() combinator now accepts an optional label parameter, solving a common pain point where developers had to choose between clean code structure and organized help output:

// Before: unlabeled merged options appeared scattered
const config = merge(connectionOptions, performanceOptions);

// Now: group related options under a clear section
const config = merge(
  "Server Configuration",  // New label parameter
  connectionOptions,
  performanceOptions
);

This simple addition makes a huge difference in help text readability, especially for CLIs with many options spread across multiple reusable modules.

The resulting help output clearly organizes options under the Server Configuration section:

Demo app showcasing labeled merge groups
Usage: demo-merge.ts --host STRING --port INTEGER --timeout INTEGER --retries
       INTEGER

Server Configuration:
  --host STRING               Server hostname or IP address
  --port INTEGER              Port number for the connection
  --timeout INTEGER           Connection timeout in seconds
  --retries INTEGER           Number of retry attempts

The new group() combinator

For cases where merge() doesn't apply, the new group() combinator lets you wrap any parser with a documentation label:

// Group mutually exclusive options under a clear section
const outputFormat = group(
  "Output Format",
  or(
    map(flag("--json"), () => "json"),
    map(flag("--yaml"), () => "yaml"),
    map(flag("--xml"), () => "xml"),
  )
);

This is particularly useful for organizing mutually exclusive flags, multiple inputs, or any parser that doesn't natively support labeling. The resulting help text becomes much more scannable and user-friendly.

Here's how the grouped output format options appear in the help text:

Demo app showcasing group combinator
Usage: demo-group.ts --json
       demo-group.ts --yaml
       demo-group.ts --xml

Output Format:
  --json                      Output in JSON format
  --yaml                      Output in YAML format
  --xml                       Output in XML format

Rich documentation support

Optique 0.4.0 introduces comprehensive documentation fields that can be added directly through the run() function, eliminating the need to modify parser definitions for documentation purposes.

Brief descriptions, detailed explanations, and footers

Both @optique/core/facade and @optique/run now support brief, description, and footer options through the run() function:

import { run } from "@optique/run";
import { message } from "@optique/core/message";

const result = run(parser, {
  brief: message`A powerful data processing tool`,
  description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations. It can handle JSON, YAML, and CSV files with automatic format detection.`,
  footer: message`Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs`,
  help: "option"
});

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

The complete help output demonstrates the rich documentation features with brief description, detailed explanation, option descriptions, default values, and footer information:

A powerful data processing tool
Usage: demo-rich-docs.ts [--port INTEGER] [--format STRING] --verbose STRING

This tool provides comprehensive data processing capabilities with support for
multiple formats and transformations. It can handle JSON, YAML, and CSV files
with automatic format detection.

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]
  --verbose STRING            Verbosity level

Examples:
  myapp process data.json --format yaml
  myapp validate config.toml --strict

For more information, visit https://example.com/docs

These documentation fields appear in both help output and error messages (when configured), providing consistent context throughout your CLI's user experience.

Display default values

A frequently requested feature is now available: showing default values directly in help text. Enable this with the new showDefault option when using withDefault():

const parser = object({
  port: withDefault(
    option("--port", integer(), { description: message`Server port number` }),
    3000,
  ),
  format: withDefault(
    option("--format", string(), { description: message`Output format` }),
    "json",
  ),
});

run(parser, { showDefault: true });

// Or with custom formatting:
run(parser, {
  showDefault: {
    prefix: " (default: ",
    suffix: ")"
  }  // Shows: --port (default: 3000)
});

Default values are automatically dimmed when colors are enabled, making them visually distinct while remaining readable.

The help output shows default values clearly marked next to each option:

Usage: demo-defaults.ts [--port INTEGER] [--format STRING]

  --port INTEGER              Server port number [3000]
  --format STRING             Output format [json]

Temporal API support

Optique 0.4.0 introduces a new package, @optique/temporal, providing comprehensive support for the modern Temporal API. This brings type-safe parsing for dates, times, durations, and time zones:

import { instant, duration, zonedDateTime } from "@optique/temporal";
import { option } from "@optique/core/parser";

const parser = object({
  // Parse ISO 8601 timestamps
  timestamp: option("--at", instant()),

  // Parse durations like "PT30M" or "P1DT2H"
  timeout: option("--timeout", duration()),

  // Parse zoned datetime with timezone info
  meeting: option("--meeting", zonedDateTime()),
});

The temporal parsers return native Temporal objects with full functionality:

const result = parse(timestampArg, ["2023-12-25T10:30:00Z"]);
if (result.success) {
  const instant = result.value;
  console.log(`UTC: ${instant.toString()}`);
  console.log(`Seoul: ${instant.toZonedDateTimeISO("Asia/Seoul")}`);
}

Install the new package with:

npm add @optique/temporal

Improved type inference

The merge() combinator now supports up to 10 parsers (previously 5), and the tuple() parser has improved type inference using TypeScript's const type parameter. These enhancements enable more complex CLI structures while maintaining perfect type safety.

Breaking changes

While we've maintained backward compatibility for most APIs, there are a few changes to be aware of:

  • The Parser.getDocFragments() method now uses DocState<TState> instead of direct state values (only affects custom parser implementations)
  • The merge() combinator now enforces stricter type constraints at compile time, rejecting non-object-producing parsers

Learn more

For a complete list of changes, bug fixes, and improvements, see the full changelog.

Check out the updated documentation:

Installation

Upgrade to Optique 0.4.0:

npm update @optique/core @optique/run
# or
deno add jsr:@optique/core@^0.4.0 jsr:@optique/run@^0.4.0

Add temporal support (optional):

npm add @optique/temporal
# or
deno add jsr:@optique/temporal

We hope these improvements make building CLI applications with Optique even more enjoyable. As always, we welcome your feedback and contributions on GitHub.

github.com

GitHub - dahlia/optique: Type-safe combinatorial CLI parser for TypeScript

Type-safe combinatorial CLI parser for TypeScript. Contribute to dahlia/optique development by creating an account on GitHub.

@elena@aseachange.com

🏕️ my adventures in #selfhosting: day 259 (slow down edition) 🐌​

a blog post that discusses the sense of urgency I felt to learn #Docker (because it will become mandatory for #Ghost)... and how a recent discovery has pushed back my deadline. I can learn slowly, yay!

🔗​: https://news.elenarossini.com/my-so-called-sudo-life/my-adventures-in-self-hosting-day-259-slow-down-edition/

#MySoCalledSudoLife #CLI

news.elenarossini.com

🏕️ my adventures in self-hosting: day 259 (slow down edition) 🐌

I am no longer in a rush to learn Docker ASAP (insert huge sigh of relief)

@quincy@chaos.social

: extract structure from a blob of data

cat very-large-file.jsonl | jq -r 'paths(scalars) | map(tostring) | join(".")' | perl -ne 's/[.][0-9]+(?=[.]|$)/.[]/g; print' | python -c $'import sys\na=set()\nfor l in iter(sys.stdin.readline, ""):\n if l in a: continue\n sys.stdout.write(l)\n a.add(l)' > schema-like.txt

@pvergain@kolektiva.social

- willmcgugan.github.io/announci (2025-07-23, Announcing Toad - a universal UI for agentic coding in the terminal, by willmcgugan.github.io/, @willmcgugan)

--<--
I’m a little salty that neither Anthropic nor Google reached out to me before they released their terminal-based AI coding agents.

You see until recently I was the CEO of Textualize, a startup promoting rich applications for the terminal. Textualize didn’t make it as a company, but I take heart that we built something amazing. There is now a thriving community of folk building TUIs that I am still a part of.

So you will understand why when I finally got round to checking out Claude code and Gemini CLI, I was more interested in the terminal interface than the AI magic it was performing. And I was not impressed. Both projects suffer from jank and other glitches inherent to terminals that Textualize solved years ago.

....

I’m currently taking a year’s sabbatical. When Textualize wrapped up I genuinely thought I was sick of coding, and I would never gain be able to find joy in building things. I’m happy to be wrong about that. I still enjoy coding, and Toad feels like the perfect hobby project for my very particular set of skills. Something I can build while continuing to focus on my physical and mental health (running a startup is taxing).

So I am going to build it.

I am building it.

Here’s a quick video of Toad in its current state: youtube.com/watch?v=EKsCS54xdu

What I have in mind is a universal front-end for AI in the terminal. This includes both AI chat-bots and agentic coding. The architecture I alluded to earlier is that the front-end built with Python and Textual connects to a back-end subprocess. The back-end handles the interactions with the LLM and performs any agentic coding, while the front-end provides the user interface. The two sides communicate with each other by sending and receiving JSON over stdout and stdin.

...

Toad isn’t quite ready for a public release. It remains a tadpole for now, incubating in a private repository. But you know I’m all about FOSS, and when its ready for a public beta I will release Toad under an Open Source license.

With a bit of luck, this sabbatical needn’t eat in to my retirement fund too much. If it goes well, it may even become my full-time gig.

I will shortly invite a few tech friends and collaborators to the project. These things can’t be the work of a single individual and I am going to need feedback as I work. If you would like to be a part of that, then feel free to reach out. But please note, I would like to prioritize folk in the Open Source community who have potentially related projects.

For everyone else, I will be posting updates regularly here and on my socials (link at the bottom of the page). Expect screenshots, videos, and long form articles. Please be patient—you will be agentic toading before too long.

Looking for markdown streaming? : willmcgugan.github.io/streamin (Efficient streaming of Markdown in the terminal)
-->--

willmcgugan.github.io

Efficient streaming of Markdown in the terminal

While working on Toad, it occurred to me there was a missing feature I would need. Namely streaming markdown. When talking to an LLM via an API, the Markdown doesn’t arrive all at once. Rather you get fragments of markdown (known as tokens) which should be appended to an existing document. Until recently the only way to render this in Textual was to remove the Markdown widget and add it again with the updated markdown. This worked, but it would get slower to append content as the document grew. It wasn’t a scalable solution.

@pvergain@kolektiva.social

- willmcgugan.github.io/announci (2025-07-23, Announcing Toad - a universal UI for agentic coding in the terminal, by willmcgugan.github.io/, @willmcgugan)

--<--
I’m a little salty that neither Anthropic nor Google reached out to me before they released their terminal-based AI coding agents.

You see until recently I was the CEO of Textualize, a startup promoting rich applications for the terminal. Textualize didn’t make it as a company, but I take heart that we built something amazing. There is now a thriving community of folk building TUIs that I am still a part of.

So you will understand why when I finally got round to checking out Claude code and Gemini CLI, I was more interested in the terminal interface than the AI magic it was performing. And I was not impressed. Both projects suffer from jank and other glitches inherent to terminals that Textualize solved years ago.

....

I’m currently taking a year’s sabbatical. When Textualize wrapped up I genuinely thought I was sick of coding, and I would never gain be able to find joy in building things. I’m happy to be wrong about that. I still enjoy coding, and Toad feels like the perfect hobby project for my very particular set of skills. Something I can build while continuing to focus on my physical and mental health (running a startup is taxing).

So I am going to build it.

I am building it.

Here’s a quick video of Toad in its current state: youtube.com/watch?v=EKsCS54xdu

What I have in mind is a universal front-end for AI in the terminal. This includes both AI chat-bots and agentic coding. The architecture I alluded to earlier is that the front-end built with Python and Textual connects to a back-end subprocess. The back-end handles the interactions with the LLM and performs any agentic coding, while the front-end provides the user interface. The two sides communicate with each other by sending and receiving JSON over stdout and stdin.

...

Toad isn’t quite ready for a public release. It remains a tadpole for now, incubating in a private repository. But you know I’m all about FOSS, and when its ready for a public beta I will release Toad under an Open Source license.

With a bit of luck, this sabbatical needn’t eat in to my retirement fund too much. If it goes well, it may even become my full-time gig.

I will shortly invite a few tech friends and collaborators to the project. These things can’t be the work of a single individual and I am going to need feedback as I work. If you would like to be a part of that, then feel free to reach out. But please note, I would like to prioritize folk in the Open Source community who have potentially related projects.

For everyone else, I will be posting updates regularly here and on my socials (link at the bottom of the page). Expect screenshots, videos, and long form articles. Please be patient—you will be agentic toading before too long.

Looking for markdown streaming? : willmcgugan.github.io/streamin (Efficient streaming of Markdown in the terminal)
-->--

willmcgugan.github.io

Efficient streaming of Markdown in the terminal

While working on Toad, it occurred to me there was a missing feature I would need. Namely streaming markdown. When talking to an LLM via an API, the Markdown doesn’t arrive all at once. Rather you get fragments of markdown (known as tokens) which should be appended to an existing document. Until recently the only way to render this in Textual was to remove the Markdown widget and add it again with the updated markdown. This worked, but it would get slower to append content as the document grew. It wasn’t a scalable solution.

@kodingwarrior@hackers.pub

이 글(그리고 후속작이 될 글들)은, 내 개발환경에서 자주 사용하고 있고 실제로도 애정하고 있는 도구에 대해서 소개하게 될 것 같다. 내가 어떤 환경에서 개발하고 있는지 궁금한 분들은 내 dotfiles 리포지토리를 참고해도 좋을 것 같다.

먼저, dotfiles란 무엇인가?

dotfiles라는 이름 자체만 보면 뭔가 대단해보일 것 같지만, dotfiles라는 이름 자체는 그냥 단순하다.

  • 어딘가의 가이드라인에서 지시하는대로 개발환경을 세팅하다보면, .zshrc/.bashrc 같은 것들을 마주하게 될 것이다.
  • git을 사용하고 있는 사람이라면, 어떤 diff 도구(delta, difft)를 사용할지, 어떤 alias 명령어를 등록할지 같은 것들을 명시하기 위해서 .gitconfig 같은 파일을 수정하게 될 때가 있다.
    • 물론, 이런것보다는 git config pull.rebase true 같은 명령어를 실행하는 경우가 더 많을 수 있다. 하지만, 이런 명령어를 실행하면 .gitconfig에도 그대로 기록이 된다.

위에서 언급한 예시를 보면 알 수 있겠듯이, 앞에 dot(.)이 붙어있는 설정파일이라면 dotfiles라고 할 수 있겠다. dotfiles 리포지토리를 만들어서 관리를 하는 이유는 무엇인가? 그것은 바로 어떤 환경에서든 .zshrc/.gitignore 파일 같은 것들을 동일하게 사용하여 작업의 흐름을 온전히 유지할 수 있다는 장점이 있기 때문이다. dotfiles를 git과 같은 버전관리 도구로 관리할 수 있고, github 같은 저장소에 올려놓을 수 있다면.... 어떤 개발환경으로 갈아타더라도 github에서 바로 내려받고, 각 설정파일들을 옮기면 그만이기 때문에 개발환경 설정하는데 드는 시간적 비용을 굉장히 아낄 수 있다.

dotfiles를 관리하는 방법들은 여러가지 있겠지만(symbolic link를 이용한다던가 등등), 개인적으로는 chezmoi를 권장하는 바이다. chezmoi는 dotfiles들을 버전관리할 수 있게 편의성을 제공해주는 CLI 도구이다.

  • 나도 chezmoi를 엄청 애용하고 있기 때문에, 내 dotfiles를 실제로 사용해보고 싶다면, chezmoi init https://github.com/malkoG/dotfiles.git 명령을 실행해보면 된다.

다시, 본론으로 Wezterm에 대해서 알아보자.

Wezterm은 Konsole/iTerm2/Gnome Terminal/Alacritty과 같은 터미널 에뮬레이터이며, Rust 기반으로 구축이 되어 있고, GPU 가속을 지원한다. 따라서, 렌더링 자체도 어느 정도는 빠른 편이다.

주변 사람들에게 한번 써보도록 권장하는 터미널 에뮬레이터가 세 가지 정도 있는데, Alacritty/Kitty 그리고 이 글에서 소개하는 Wezterm 정도 된다. 요즘은 Zig 기반으로 만들어진 Ghostty[1]도 추천할만 한 것 같다. 하지만, 이 글에서는 Wezterm을 소개하기로 했기 때문에, Wezterm 중심으로 소개하도록 하겠다.

Wezterm은 다음과 같은 특징을 가진다.

  • Linux, MacOS, 윈도우즈, FreeBSD 다양한 환경에서 돌아간다.
  • 한 윈도우를 여러개의 pane으로 쪼개서 분할하여 멀티플렉싱을 지원한다.(물론, 나는 ZelliJ/Tmux를 쓰기때문에 잘 이용하지는 않는 기능이다.
  • 마우스 지원이 잘 된다
  • 터미널에 표시되는 글자를 캡쳐해서 하이퍼링크로 치환이 가능하다.
    • 이건 나도 잘 이용하지는 않는 기능이지만.. T0010 같은 코드가 화면 상에 보여진다면, 특정 린터 페이지의 설명화면으로 이동하는 링크를 심을 수 있다. kiyoon님의 dotfiles 참고
  • lua로 스크립팅이 가능한데, 활용할 수 있는 방향이 굉장히 다양하다.
    • 여기에서 설명한 기능들을 최대한 활용하여 스크립팅이 가능하다.
    • API 문서를 슥 훑어보았을때, "어? 설마 이게 되나?" 싶은 생각이 든다면, 여러분이 생각하는 것도 아마 가능할지도 모른다.

Wezterm, 써보자.

Wezterm을 설치한다면 설치 안내 페이지를 참고해서 설치하면 되는데, 여러분이 Wezterm을 설치했다면 당장은 검은 화면만 뜰지도 모른다.

Wezterm을 설치하고 나서, .config/wezterm/wezterm.lua 파일을 수정해야 하는데, 당장은 아래처럼 비어있을 것이다. 파일이 없다면 만들어두는 것이 좋다.

return {}

위의 코드에서 {}는 lua에서는 테이블(다른 언어로 치면, 딕셔너리/오브젝트 같은 것)이지만, wezterm 터미널의 configuration을 나타내는 테이블이다. 여기에 몇가지 추가사항을 넣어보겠다.

터미널 에뮬레이터를 설치했는데, 터미널 에뮬레이터를 설치했으면 가장 처음부터 하는게 무엇이겠는가? 바로, 폰트를 세팅하는 것이다. 터미널 환경에서 작업할때 폰트만큼 중요한게 또 없다.

local wezterm = require("wezterm")

return {
	font = wezterm.font_with_fallback({'Cascadia Code NF', 'NanumBarunGothic'}),
	font_size = 12.0,
	line_height = 1.2,
}

폰트를 가져다 쓸때는 위의 예시와 같이 font_with_fallback 함수를 이용해서 가져다 쓸 수 있고, 그 외에도 폰트 크기를 지정하거나 행간을 지정할 수도 있다. 공식페이지에서 보았듯이, 여러분의 취향에 따라 배경색 혹은 배경이미지도 지정할 수 있는데 여러분 나름대로의 기준이 있고 욕심이 난다면 한번 도전해보는 것도 나쁘지 않을 것 같다.

위의 코드를 복사 붙여넣고 편집하다보면 느낄 수 있겠지만, wezterm은 설정파일을 편집할때 Hot Reloading을 지원한다. 이 또한, 내가 가장 애정하는 기능 중 하나이다. 혹여나 wezterm 설정 파일을 수정했을때, 문법에 오류가 있거나 설정값을 잘못 지정했을 때, 새 창으로 어떤 부분에 오류가 있는지 친절하게 Alert도 띄워준다.

API 레퍼런스를 보기만 해도 스크립팅으로 기능을 확장할 수 있는 가능성이야 당연히 많긴 하겠지만, 처음 접하는 입장에서는 어떻게 커스터마이징할 지 파악하기 난해할 수 있다.

아래에서는 내가 어떻게 Wezterm을 커스터마이징을 하고 있는지 예시를 나열하는 것으로 글을 끝내겠다. Wezterm, 믿고 써보시라.

내가 Wezterm을 응용하는 방법

1. 반투명도 조정하기

Wezterm에서 터미널 색상을 설정할때, 배경색상의 반투명도를 지정할 수 있는 옵션이 있다. 나는 여기서 단축키를 입력했을때 반투명도를 동적으로 조절하고 싶었다.

반투명도를 상수로 둘 수는 있지만, 모니터 하나 짜리의 환경에서 작업한다면 브라우저를 뒤쪽에 두고 터미널 앱을 앞에 두는 식으로 작업을 많이 하게 된다. HMR(Hot Module Reloading)이 되는 개발환경이라면, 소스코드를 편집하고 화면에 즉각적으로 반영이 되는걸 기대할텐데 이걸 탭 스위칭하면서 확인하기는 굉장히 번거롭다. 온전히 작업을 유지하다가 잠깐 확인하고 싶을때 반투명도를 변경하면 되는데도 말이다.

Wezterm에서는 이벤트를 발신할 수 있고, 다른 프로세스에서 특정 이벤트를 수신했을때 어떤 동작을 할 것인지를 정의할 수 있다. Wezterm 터미널 옵션을 수정하는것도 여기에 포함될 수 있다. 특정 단축키를 입력하면, 어떤 이벤트를 발생시킬 수 있고, 그 이벤트로 인해서 화면의 반투명도를 조정할 수 있게 했다.

local default_opacity = 0.9

local keymaps = {}

-- SHIFT + CTRL + Z 키를 누르면 반투명도를 감소시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = "Z",
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'decrease-opacity',
  }
)

-- SHIFT + CTRL + X 키를 누르면 반투명도를 증가시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = 'X',
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'increase-opacity',
  }
)


-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('increase-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity
  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity + 0.1
  if opacity > 1.0 then
	opacity = 1.0
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('decrease-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity

  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity - 0.1
  if opacity < 0.3 then
	opacity = 0.3
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

return {
  keys = keymaps
}

2. 탭 이름 변경하기

대부분 터미널 에뮬레이터에서 탭 이름을 표시할때, 현재 탭에서 돌고 있는 프로세스의 이름을 명시할때가 많다. 그런데, 탭에 명시되어 있는 타이틀만 가지고는 각각의 탭이 어떤 역할을 하는 것인지 파악하기 어렵다. 어느 쪽이 서버를 띄우고 있는건지, 어느 쪽이 에디터 편집 화면인지, 어느 쪽이 LLM Agent를 돌리고 있는지 난해하다.

하지만, 아래처럼 wezterm.action.PromptInputLine API를 사용해서 프롬프트 입력을 받은 내용을 기반으로, 현재 활성화된 탭의 이름을 변경하는 식으로 인지부하를 줄일 수 있다.

local wezterm = require('wezterm')
local keymaps = {}

table.insert(
  keymaps,
  {
	key = '`',
	mods = 'CTRL',
	action = wezterm.action.PromptInputLine {
	  description = "Enter new name for tab",
	  action = wezterm.action_callback(function(window, _, line)
	    if line then
		  window:active_tab():set_title(line)
		end
	  end)
	}
  }
)

return {
  keys = keymaps
}

3. BEEP음 대신 화면이 번쩍이게 하기

Aider나 Claude Code 같은 LLM 에이전트가 작업을 수행 후 아예 작업이 끝났거나, 혹은 작업에 대한 승인을 요구할 때가 간혹 있다. LLM 에이전트가 작업을 수행하는걸 가만히 보고만 있을 순 없을 것이다.

Claude Code/Aider는 다행히도 응답을 하고 나서 추가적인 명령을 실행할 수 있는 옵션[2]을 제공해주는데, 거기에 간단하게 echo -ne '\007' 명령어를 넘겨줄 수 있다. 이 명령어는 엄청 어렵지는 않다. 터미널 앱에 내장된 시스템 벨 소리를 재생하는 명령어다.

작업 환경이 어디냐에 따라 다를 순 있겠지만, 작업 완료 여부를 음성으로 받기에는 물리적인 제약이 있을 수 있다. 아예 알림 센터를 이용한다고 치자. 업무 시간대에 방해금지 모드를 설정했다면 알림이 울리게 설정이 했더라도 시스템 제약상 묻힐 가능성도 있다.[3]

visual_bell 옵션을 활용하면, 터미널에 내장된 시스템 벨 소리를 울리는 대신 화면이 반짝이게 해서 다른 에이전트가 응답을 완료했다고 명시적으로 알림을 받을 수 있다. LLM 에이전트가 하는 일을 일일이 모니터링하지 않더라도, 다른 작업을 수행하는 중에 화면이 반짝이면 그때 확인하기만 하면 그만이다.

return {
	colors = {
		visual_bell = '#003355',
	},

	visual_bell = {
		fade_in_duration_ms  = 75,
		fade_out_duration_ms = 75,
		target               = 'BackgroundColor', -- 또는 'CursorColor'
	}
}

  1. Hashicorp 공동창업자인 Mitchell Hashimoto가 만든 터미널 에뮬레이터인데, OpenAI에서 Codex 시연할때 Ghostty 터미널로 시연한 바가 있다. ↩︎

  2. Aider는 notifications-command, Claude Code는 Hook이 있다 ↩︎

  3. 어떤 사람은 휴대전화를 진동모드로 해놓고, 텔레그램/디스코드로 알림쏴서 알림을 수신받는 방식을 쓰고 있는 것을 관찰한 바는 있다. ↩︎

@kodingwarrior@hackers.pub

이 글(그리고 후속작이 될 글들)은, 내 개발환경에서 자주 사용하고 있고 실제로도 애정하고 있는 도구에 대해서 소개하게 될 것 같다. 내가 어떤 환경에서 개발하고 있는지 궁금한 분들은 내 dotfiles 리포지토리를 참고해도 좋을 것 같다.

먼저, dotfiles란 무엇인가?

dotfiles라는 이름 자체만 보면 뭔가 대단해보일 것 같지만, dotfiles라는 이름 자체는 그냥 단순하다.

  • 어딘가의 가이드라인에서 지시하는대로 개발환경을 세팅하다보면, .zshrc/.bashrc 같은 것들을 마주하게 될 것이다.
  • git을 사용하고 있는 사람이라면, 어떤 diff 도구(delta, difft)를 사용할지, 어떤 alias 명령어를 등록할지 같은 것들을 명시하기 위해서 .gitconfig 같은 파일을 수정하게 될 때가 있다.
    • 물론, 이런것보다는 git config pull.rebase true 같은 명령어를 실행하는 경우가 더 많을 수 있다. 하지만, 이런 명령어를 실행하면 .gitconfig에도 그대로 기록이 된다.

위에서 언급한 예시를 보면 알 수 있겠듯이, 앞에 dot(.)이 붙어있는 설정파일이라면 dotfiles라고 할 수 있겠다. dotfiles 리포지토리를 만들어서 관리를 하는 이유는 무엇인가? 그것은 바로 어떤 환경에서든 .zshrc/.gitignore 파일 같은 것들을 동일하게 사용하여 작업의 흐름을 온전히 유지할 수 있다는 장점이 있기 때문이다. dotfiles를 git과 같은 버전관리 도구로 관리할 수 있고, github 같은 저장소에 올려놓을 수 있다면.... 어떤 개발환경으로 갈아타더라도 github에서 바로 내려받고, 각 설정파일들을 옮기면 그만이기 때문에 개발환경 설정하는데 드는 시간적 비용을 굉장히 아낄 수 있다.

dotfiles를 관리하는 방법들은 여러가지 있겠지만(symbolic link를 이용한다던가 등등), 개인적으로는 chezmoi를 권장하는 바이다. chezmoi는 dotfiles들을 버전관리할 수 있게 편의성을 제공해주는 CLI 도구이다.

  • 나도 chezmoi를 엄청 애용하고 있기 때문에, 내 dotfiles를 실제로 사용해보고 싶다면, chezmoi init https://github.com/malkoG/dotfiles.git 명령을 실행해보면 된다.

다시, 본론으로 Wezterm에 대해서 알아보자.

Wezterm은 Konsole/iTerm2/Gnome Terminal/Alacritty과 같은 터미널 에뮬레이터이며, Rust 기반으로 구축이 되어 있고, GPU 가속을 지원한다. 따라서, 렌더링 자체도 어느 정도는 빠른 편이다.

주변 사람들에게 한번 써보도록 권장하는 터미널 에뮬레이터가 세 가지 정도 있는데, Alacritty/Kitty 그리고 이 글에서 소개하는 Wezterm 정도 된다. 요즘은 Zig 기반으로 만들어진 Ghostty[1]도 추천할만 한 것 같다. 하지만, 이 글에서는 Wezterm을 소개하기로 했기 때문에, Wezterm 중심으로 소개하도록 하겠다.

Wezterm은 다음과 같은 특징을 가진다.

  • Linux, MacOS, 윈도우즈, FreeBSD 다양한 환경에서 돌아간다.
  • 한 윈도우를 여러개의 pane으로 쪼개서 분할하여 멀티플렉싱을 지원한다.(물론, 나는 ZelliJ/Tmux를 쓰기때문에 잘 이용하지는 않는 기능이다.
  • 마우스 지원이 잘 된다
  • 터미널에 표시되는 글자를 캡쳐해서 하이퍼링크로 치환이 가능하다.
    • 이건 나도 잘 이용하지는 않는 기능이지만.. T0010 같은 코드가 화면 상에 보여진다면, 특정 린터 페이지의 설명화면으로 이동하는 링크를 심을 수 있다. kiyoon님의 dotfiles 참고
  • lua로 스크립팅이 가능한데, 활용할 수 있는 방향이 굉장히 다양하다.
    • 여기에서 설명한 기능들을 최대한 활용하여 스크립팅이 가능하다.
    • API 문서를 슥 훑어보았을때, "어? 설마 이게 되나?" 싶은 생각이 든다면, 여러분이 생각하는 것도 아마 가능할지도 모른다.

Wezterm, 써보자.

Wezterm을 설치한다면 설치 안내 페이지를 참고해서 설치하면 되는데, 여러분이 Wezterm을 설치했다면 당장은 검은 화면만 뜰지도 모른다.

Wezterm을 설치하고 나서, .config/wezterm/wezterm.lua 파일을 수정해야 하는데, 당장은 아래처럼 비어있을 것이다. 파일이 없다면 만들어두는 것이 좋다.

return {}

위의 코드에서 {}는 lua에서는 테이블(다른 언어로 치면, 딕셔너리/오브젝트 같은 것)이지만, wezterm 터미널의 configuration을 나타내는 테이블이다. 여기에 몇가지 추가사항을 넣어보겠다.

터미널 에뮬레이터를 설치했는데, 터미널 에뮬레이터를 설치했으면 가장 처음부터 하는게 무엇이겠는가? 바로, 폰트를 세팅하는 것이다. 터미널 환경에서 작업할때 폰트만큼 중요한게 또 없다.

local wezterm = require("wezterm")

return {
	font = wezterm.font_with_fallback({'Cascadia Code NF', 'NanumBarunGothic'}),
	font_size = 12.0,
	line_height = 1.2,
}

폰트를 가져다 쓸때는 위의 예시와 같이 font_with_fallback 함수를 이용해서 가져다 쓸 수 있고, 그 외에도 폰트 크기를 지정하거나 행간을 지정할 수도 있다. 공식페이지에서 보았듯이, 여러분의 취향에 따라 배경색 혹은 배경이미지도 지정할 수 있는데 여러분 나름대로의 기준이 있고 욕심이 난다면 한번 도전해보는 것도 나쁘지 않을 것 같다.

위의 코드를 복사 붙여넣고 편집하다보면 느낄 수 있겠지만, wezterm은 설정파일을 편집할때 Hot Reloading을 지원한다. 이 또한, 내가 가장 애정하는 기능 중 하나이다. 혹여나 wezterm 설정 파일을 수정했을때, 문법에 오류가 있거나 설정값을 잘못 지정했을 때, 새 창으로 어떤 부분에 오류가 있는지 친절하게 Alert도 띄워준다.

API 레퍼런스를 보기만 해도 스크립팅으로 기능을 확장할 수 있는 가능성이야 당연히 많긴 하겠지만, 처음 접하는 입장에서는 어떻게 커스터마이징할 지 파악하기 난해할 수 있다.

아래에서는 내가 어떻게 Wezterm을 커스터마이징을 하고 있는지 예시를 나열하는 것으로 글을 끝내겠다. Wezterm, 믿고 써보시라.

내가 Wezterm을 응용하는 방법

1. 반투명도 조정하기

Wezterm에서 터미널 색상을 설정할때, 배경색상의 반투명도를 지정할 수 있는 옵션이 있다. 나는 여기서 단축키를 입력했을때 반투명도를 동적으로 조절하고 싶었다.

반투명도를 상수로 둘 수는 있지만, 모니터 하나 짜리의 환경에서 작업한다면 브라우저를 뒤쪽에 두고 터미널 앱을 앞에 두는 식으로 작업을 많이 하게 된다. HMR(Hot Module Reloading)이 되는 개발환경이라면, 소스코드를 편집하고 화면에 즉각적으로 반영이 되는걸 기대할텐데 이걸 탭 스위칭하면서 확인하기는 굉장히 번거롭다. 온전히 작업을 유지하다가 잠깐 확인하고 싶을때 반투명도를 변경하면 되는데도 말이다.

Wezterm에서는 이벤트를 발신할 수 있고, 다른 프로세스에서 특정 이벤트를 수신했을때 어떤 동작을 할 것인지를 정의할 수 있다. Wezterm 터미널 옵션을 수정하는것도 여기에 포함될 수 있다. 특정 단축키를 입력하면, 어떤 이벤트를 발생시킬 수 있고, 그 이벤트로 인해서 화면의 반투명도를 조정할 수 있게 했다.

local default_opacity = 0.9

local keymaps = {}

-- SHIFT + CTRL + Z 키를 누르면 반투명도를 감소시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = "Z",
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'decrease-opacity',
  }
)

-- SHIFT + CTRL + X 키를 누르면 반투명도를 증가시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = 'X',
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'increase-opacity',
  }
)


-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('increase-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity
  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity + 0.1
  if opacity > 1.0 then
	opacity = 1.0
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('decrease-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity

  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity - 0.1
  if opacity < 0.3 then
	opacity = 0.3
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

return {
  keys = keymaps
}

2. 탭 이름 변경하기

대부분 터미널 에뮬레이터에서 탭 이름을 표시할때, 현재 탭에서 돌고 있는 프로세스의 이름을 명시할때가 많다. 그런데, 탭에 명시되어 있는 타이틀만 가지고는 각각의 탭이 어떤 역할을 하는 것인지 파악하기 어렵다. 어느 쪽이 서버를 띄우고 있는건지, 어느 쪽이 에디터 편집 화면인지, 어느 쪽이 LLM Agent를 돌리고 있는지 난해하다.

하지만, 아래처럼 wezterm.action.PromptInputLine API를 사용해서 프롬프트 입력을 받은 내용을 기반으로, 현재 활성화된 탭의 이름을 변경하는 식으로 인지부하를 줄일 수 있다.

local wezterm = require('wezterm')
local keymaps = {}

table.insert(
  keymaps,
  {
	key = '`',
	mods = 'CTRL',
	action = wezterm.action.PromptInputLine {
	  description = "Enter new name for tab",
	  action = wezterm.action_callback(function(window, _, line)
	    if line then
		  window:active_tab():set_title(line)
		end
	  end)
	}
  }
)

return {
  keys = keymaps
}

3. BEEP음 대신 화면이 번쩍이게 하기

Aider나 Claude Code 같은 LLM 에이전트가 작업을 수행 후 아예 작업이 끝났거나, 혹은 작업에 대한 승인을 요구할 때가 간혹 있다. LLM 에이전트가 작업을 수행하는걸 가만히 보고만 있을 순 없을 것이다.

Claude Code/Aider는 다행히도 응답을 하고 나서 추가적인 명령을 실행할 수 있는 옵션[2]을 제공해주는데, 거기에 간단하게 echo -ne '\007' 명령어를 넘겨줄 수 있다. 이 명령어는 엄청 어렵지는 않다. 터미널 앱에 내장된 시스템 벨 소리를 재생하는 명령어다.

작업 환경이 어디냐에 따라 다를 순 있겠지만, 작업 완료 여부를 음성으로 받기에는 물리적인 제약이 있을 수 있다. 아예 알림 센터를 이용한다고 치자. 업무 시간대에 방해금지 모드를 설정했다면 알림이 울리게 설정이 했더라도 시스템 제약상 묻힐 가능성도 있다.[3]

visual_bell 옵션을 활용하면, 터미널에 내장된 시스템 벨 소리를 울리는 대신 화면이 반짝이게 해서 다른 에이전트가 응답을 완료했다고 명시적으로 알림을 받을 수 있다. LLM 에이전트가 하는 일을 일일이 모니터링하지 않더라도, 다른 작업을 수행하는 중에 화면이 반짝이면 그때 확인하기만 하면 그만이다.

return {
	colors = {
		visual_bell = '#003355',
	},

	visual_bell = {
		fade_in_duration_ms  = 75,
		fade_out_duration_ms = 75,
		target               = 'BackgroundColor', -- 또는 'CursorColor'
	}
}

  1. Hashicorp 공동창업자인 Mitchell Hashimoto가 만든 터미널 에뮬레이터인데, OpenAI에서 Codex 시연할때 Ghostty 터미널로 시연한 바가 있다. ↩︎

  2. Aider는 notifications-command, Claude Code는 Hook이 있다 ↩︎

  3. 어떤 사람은 휴대전화를 진동모드로 해놓고, 텔레그램/디스코드로 알림쏴서 알림을 수신받는 방식을 쓰고 있는 것을 관찰한 바는 있다. ↩︎

@kodingwarrior@hackers.pub

이 글(그리고 후속작이 될 글들)은, 내 개발환경에서 자주 사용하고 있고 실제로도 애정하고 있는 도구에 대해서 소개하게 될 것 같다. 내가 어떤 환경에서 개발하고 있는지 궁금한 분들은 내 dotfiles 리포지토리를 참고해도 좋을 것 같다.

먼저, dotfiles란 무엇인가?

dotfiles라는 이름 자체만 보면 뭔가 대단해보일 것 같지만, dotfiles라는 이름 자체는 그냥 단순하다.

  • 어딘가의 가이드라인에서 지시하는대로 개발환경을 세팅하다보면, .zshrc/.bashrc 같은 것들을 마주하게 될 것이다.
  • git을 사용하고 있는 사람이라면, 어떤 diff 도구(delta, difft)를 사용할지, 어떤 alias 명령어를 등록할지 같은 것들을 명시하기 위해서 .gitconfig 같은 파일을 수정하게 될 때가 있다.
    • 물론, 이런것보다는 git config pull.rebase true 같은 명령어를 실행하는 경우가 더 많을 수 있다. 하지만, 이런 명령어를 실행하면 .gitconfig에도 그대로 기록이 된다.

위에서 언급한 예시를 보면 알 수 있겠듯이, 앞에 dot(.)이 붙어있는 설정파일이라면 dotfiles라고 할 수 있겠다. dotfiles 리포지토리를 만들어서 관리를 하는 이유는 무엇인가? 그것은 바로 어떤 환경에서든 .zshrc/.gitignore 파일 같은 것들을 동일하게 사용하여 작업의 흐름을 온전히 유지할 수 있다는 장점이 있기 때문이다. dotfiles를 git과 같은 버전관리 도구로 관리할 수 있고, github 같은 저장소에 올려놓을 수 있다면.... 어떤 개발환경으로 갈아타더라도 github에서 바로 내려받고, 각 설정파일들을 옮기면 그만이기 때문에 개발환경 설정하는데 드는 시간적 비용을 굉장히 아낄 수 있다.

dotfiles를 관리하는 방법들은 여러가지 있겠지만(symbolic link를 이용한다던가 등등), 개인적으로는 chezmoi를 권장하는 바이다. chezmoi는 dotfiles들을 버전관리할 수 있게 편의성을 제공해주는 CLI 도구이다.

  • 나도 chezmoi를 엄청 애용하고 있기 때문에, 내 dotfiles를 실제로 사용해보고 싶다면, chezmoi init https://github.com/malkoG/dotfiles.git 명령을 실행해보면 된다.

다시, 본론으로 Wezterm에 대해서 알아보자.

Wezterm은 Konsole/iTerm2/Gnome Terminal/Alacritty과 같은 터미널 에뮬레이터이며, Rust 기반으로 구축이 되어 있고, GPU 가속을 지원한다. 따라서, 렌더링 자체도 어느 정도는 빠른 편이다.

주변 사람들에게 한번 써보도록 권장하는 터미널 에뮬레이터가 세 가지 정도 있는데, Alacritty/Kitty 그리고 이 글에서 소개하는 Wezterm 정도 된다. 요즘은 Zig 기반으로 만들어진 Ghostty[1]도 추천할만 한 것 같다. 하지만, 이 글에서는 Wezterm을 소개하기로 했기 때문에, Wezterm 중심으로 소개하도록 하겠다.

Wezterm은 다음과 같은 특징을 가진다.

  • Linux, MacOS, 윈도우즈, FreeBSD 다양한 환경에서 돌아간다.
  • 한 윈도우를 여러개의 pane으로 쪼개서 분할하여 멀티플렉싱을 지원한다.(물론, 나는 ZelliJ/Tmux를 쓰기때문에 잘 이용하지는 않는 기능이다.
  • 마우스 지원이 잘 된다
  • 터미널에 표시되는 글자를 캡쳐해서 하이퍼링크로 치환이 가능하다.
    • 이건 나도 잘 이용하지는 않는 기능이지만.. T0010 같은 코드가 화면 상에 보여진다면, 특정 린터 페이지의 설명화면으로 이동하는 링크를 심을 수 있다. kiyoon님의 dotfiles 참고
  • lua로 스크립팅이 가능한데, 활용할 수 있는 방향이 굉장히 다양하다.
    • 여기에서 설명한 기능들을 최대한 활용하여 스크립팅이 가능하다.
    • API 문서를 슥 훑어보았을때, "어? 설마 이게 되나?" 싶은 생각이 든다면, 여러분이 생각하는 것도 아마 가능할지도 모른다.

Wezterm, 써보자.

Wezterm을 설치한다면 설치 안내 페이지를 참고해서 설치하면 되는데, 여러분이 Wezterm을 설치했다면 당장은 검은 화면만 뜰지도 모른다.

Wezterm을 설치하고 나서, .config/wezterm/wezterm.lua 파일을 수정해야 하는데, 당장은 아래처럼 비어있을 것이다. 파일이 없다면 만들어두는 것이 좋다.

return {}

위의 코드에서 {}는 lua에서는 테이블(다른 언어로 치면, 딕셔너리/오브젝트 같은 것)이지만, wezterm 터미널의 configuration을 나타내는 테이블이다. 여기에 몇가지 추가사항을 넣어보겠다.

터미널 에뮬레이터를 설치했는데, 터미널 에뮬레이터를 설치했으면 가장 처음부터 하는게 무엇이겠는가? 바로, 폰트를 세팅하는 것이다. 터미널 환경에서 작업할때 폰트만큼 중요한게 또 없다.

local wezterm = require("wezterm")

return {
	font = wezterm.font_with_fallback({'Cascadia Code NF', 'NanumBarunGothic'}),
	font_size = 12.0,
	line_height = 1.2,
}

폰트를 가져다 쓸때는 위의 예시와 같이 font_with_fallback 함수를 이용해서 가져다 쓸 수 있고, 그 외에도 폰트 크기를 지정하거나 행간을 지정할 수도 있다. 공식페이지에서 보았듯이, 여러분의 취향에 따라 배경색 혹은 배경이미지도 지정할 수 있는데 여러분 나름대로의 기준이 있고 욕심이 난다면 한번 도전해보는 것도 나쁘지 않을 것 같다.

위의 코드를 복사 붙여넣고 편집하다보면 느낄 수 있겠지만, wezterm은 설정파일을 편집할때 Hot Reloading을 지원한다. 이 또한, 내가 가장 애정하는 기능 중 하나이다. 혹여나 wezterm 설정 파일을 수정했을때, 문법에 오류가 있거나 설정값을 잘못 지정했을 때, 새 창으로 어떤 부분에 오류가 있는지 친절하게 Alert도 띄워준다.

API 레퍼런스를 보기만 해도 스크립팅으로 기능을 확장할 수 있는 가능성이야 당연히 많긴 하겠지만, 처음 접하는 입장에서는 어떻게 커스터마이징할 지 파악하기 난해할 수 있다.

아래에서는 내가 어떻게 Wezterm을 커스터마이징을 하고 있는지 예시를 나열하는 것으로 글을 끝내겠다. Wezterm, 믿고 써보시라.

내가 Wezterm을 응용하는 방법

1. 반투명도 조정하기

Wezterm에서 터미널 색상을 설정할때, 배경색상의 반투명도를 지정할 수 있는 옵션이 있다. 나는 여기서 단축키를 입력했을때 반투명도를 동적으로 조절하고 싶었다.

반투명도를 상수로 둘 수는 있지만, 모니터 하나 짜리의 환경에서 작업한다면 브라우저를 뒤쪽에 두고 터미널 앱을 앞에 두는 식으로 작업을 많이 하게 된다. HMR(Hot Module Reloading)이 되는 개발환경이라면, 소스코드를 편집하고 화면에 즉각적으로 반영이 되는걸 기대할텐데 이걸 탭 스위칭하면서 확인하기는 굉장히 번거롭다. 온전히 작업을 유지하다가 잠깐 확인하고 싶을때 반투명도를 변경하면 되는데도 말이다.

Wezterm에서는 이벤트를 발신할 수 있고, 다른 프로세스에서 특정 이벤트를 수신했을때 어떤 동작을 할 것인지를 정의할 수 있다. Wezterm 터미널 옵션을 수정하는것도 여기에 포함될 수 있다. 특정 단축키를 입력하면, 어떤 이벤트를 발생시킬 수 있고, 그 이벤트로 인해서 화면의 반투명도를 조정할 수 있게 했다.

local default_opacity = 0.9

local keymaps = {}

-- SHIFT + CTRL + Z 키를 누르면 반투명도를 감소시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = "Z",
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'decrease-opacity',
  }
)

-- SHIFT + CTRL + X 키를 누르면 반투명도를 증가시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = 'X',
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'increase-opacity',
  }
)


-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('increase-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity
  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity + 0.1
  if opacity > 1.0 then
	opacity = 1.0
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('decrease-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity

  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity - 0.1
  if opacity < 0.3 then
	opacity = 0.3
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

return {
  keys = keymaps
}

2. 탭 이름 변경하기

대부분 터미널 에뮬레이터에서 탭 이름을 표시할때, 현재 탭에서 돌고 있는 프로세스의 이름을 명시할때가 많다. 그런데, 탭에 명시되어 있는 타이틀만 가지고는 각각의 탭이 어떤 역할을 하는 것인지 파악하기 어렵다. 어느 쪽이 서버를 띄우고 있는건지, 어느 쪽이 에디터 편집 화면인지, 어느 쪽이 LLM Agent를 돌리고 있는지 난해하다.

하지만, 아래처럼 wezterm.action.PromptInputLine API를 사용해서 프롬프트 입력을 받은 내용을 기반으로, 현재 활성화된 탭의 이름을 변경하는 식으로 인지부하를 줄일 수 있다.

local wezterm = require('wezterm')
local keymaps = {}

table.insert(
  keymaps,
  {
	key = '`',
	mods = 'CTRL',
	action = wezterm.action.PromptInputLine {
	  description = "Enter new name for tab",
	  action = wezterm.action_callback(function(window, _, line)
	    if line then
		  window:active_tab():set_title(line)
		end
	  end)
	}
  }
)

return {
  keys = keymaps
}

3. BEEP음 대신 화면이 번쩍이게 하기

Aider나 Claude Code 같은 LLM 에이전트가 작업을 수행 후 아예 작업이 끝났거나, 혹은 작업에 대한 승인을 요구할 때가 간혹 있다. LLM 에이전트가 작업을 수행하는걸 가만히 보고만 있을 순 없을 것이다.

Claude Code/Aider는 다행히도 응답을 하고 나서 추가적인 명령을 실행할 수 있는 옵션[2]을 제공해주는데, 거기에 간단하게 echo -ne '\007' 명령어를 넘겨줄 수 있다. 이 명령어는 엄청 어렵지는 않다. 터미널 앱에 내장된 시스템 벨 소리를 재생하는 명령어다.

작업 환경이 어디냐에 따라 다를 순 있겠지만, 작업 완료 여부를 음성으로 받기에는 물리적인 제약이 있을 수 있다. 아예 알림 센터를 이용한다고 치자. 업무 시간대에 방해금지 모드를 설정했다면 알림이 울리게 설정이 했더라도 시스템 제약상 묻힐 가능성도 있다.[3]

visual_bell 옵션을 활용하면, 터미널에 내장된 시스템 벨 소리를 울리는 대신 화면이 반짝이게 해서 다른 에이전트가 응답을 완료했다고 명시적으로 알림을 받을 수 있다. LLM 에이전트가 하는 일을 일일이 모니터링하지 않더라도, 다른 작업을 수행하는 중에 화면이 반짝이면 그때 확인하기만 하면 그만이다.

return {
	colors = {
		visual_bell = '#003355',
	},

	visual_bell = {
		fade_in_duration_ms  = 75,
		fade_out_duration_ms = 75,
		target               = 'BackgroundColor', -- 또는 'CursorColor'
	}
}

  1. Hashicorp 공동창업자인 Mitchell Hashimoto가 만든 터미널 에뮬레이터인데, OpenAI에서 Codex 시연할때 Ghostty 터미널로 시연한 바가 있다. ↩︎

  2. Aider는 notifications-command, Claude Code는 Hook이 있다 ↩︎

  3. 어떤 사람은 휴대전화를 진동모드로 해놓고, 텔레그램/디스코드로 알림쏴서 알림을 수신받는 방식을 쓰고 있는 것을 관찰한 바는 있다. ↩︎

@kodingwarrior@hackers.pub

이 글(그리고 후속작이 될 글들)은, 내 개발환경에서 자주 사용하고 있고 실제로도 애정하고 있는 도구에 대해서 소개하게 될 것 같다. 내가 어떤 환경에서 개발하고 있는지 궁금한 분들은 내 dotfiles 리포지토리를 참고해도 좋을 것 같다.

먼저, dotfiles란 무엇인가?

dotfiles라는 이름 자체만 보면 뭔가 대단해보일 것 같지만, dotfiles라는 이름 자체는 그냥 단순하다.

  • 어딘가의 가이드라인에서 지시하는대로 개발환경을 세팅하다보면, .zshrc/.bashrc 같은 것들을 마주하게 될 것이다.
  • git을 사용하고 있는 사람이라면, 어떤 diff 도구(delta, difft)를 사용할지, 어떤 alias 명령어를 등록할지 같은 것들을 명시하기 위해서 .gitconfig 같은 파일을 수정하게 될 때가 있다.
    • 물론, 이런것보다는 git config pull.rebase true 같은 명령어를 실행하는 경우가 더 많을 수 있다. 하지만, 이런 명령어를 실행하면 .gitconfig에도 그대로 기록이 된다.

위에서 언급한 예시를 보면 알 수 있겠듯이, 앞에 dot(.)이 붙어있는 설정파일이라면 dotfiles라고 할 수 있겠다. dotfiles 리포지토리를 만들어서 관리를 하는 이유는 무엇인가? 그것은 바로 어떤 환경에서든 .zshrc/.gitignore 파일 같은 것들을 동일하게 사용하여 작업의 흐름을 온전히 유지할 수 있다는 장점이 있기 때문이다. dotfiles를 git과 같은 버전관리 도구로 관리할 수 있고, github 같은 저장소에 올려놓을 수 있다면.... 어떤 개발환경으로 갈아타더라도 github에서 바로 내려받고, 각 설정파일들을 옮기면 그만이기 때문에 개발환경 설정하는데 드는 시간적 비용을 굉장히 아낄 수 있다.

dotfiles를 관리하는 방법들은 여러가지 있겠지만(symbolic link를 이용한다던가 등등), 개인적으로는 chezmoi를 권장하는 바이다. chezmoi는 dotfiles들을 버전관리할 수 있게 편의성을 제공해주는 CLI 도구이다.

  • 나도 chezmoi를 엄청 애용하고 있기 때문에, 내 dotfiles를 실제로 사용해보고 싶다면, chezmoi init https://github.com/malkoG/dotfiles.git 명령을 실행해보면 된다.

다시, 본론으로 Wezterm에 대해서 알아보자.

Wezterm은 Konsole/iTerm2/Gnome Terminal/Alacritty과 같은 터미널 에뮬레이터이며, Rust 기반으로 구축이 되어 있고, GPU 가속을 지원한다. 따라서, 렌더링 자체도 어느 정도는 빠른 편이다.

주변 사람들에게 한번 써보도록 권장하는 터미널 에뮬레이터가 세 가지 정도 있는데, Alacritty/Kitty 그리고 이 글에서 소개하는 Wezterm 정도 된다. 요즘은 Zig 기반으로 만들어진 Ghostty[1]도 추천할만 한 것 같다. 하지만, 이 글에서는 Wezterm을 소개하기로 했기 때문에, Wezterm 중심으로 소개하도록 하겠다.

Wezterm은 다음과 같은 특징을 가진다.

  • Linux, MacOS, 윈도우즈, FreeBSD 다양한 환경에서 돌아간다.
  • 한 윈도우를 여러개의 pane으로 쪼개서 분할하여 멀티플렉싱을 지원한다.(물론, 나는 ZelliJ/Tmux를 쓰기때문에 잘 이용하지는 않는 기능이다.
  • 마우스 지원이 잘 된다
  • 터미널에 표시되는 글자를 캡쳐해서 하이퍼링크로 치환이 가능하다.
    • 이건 나도 잘 이용하지는 않는 기능이지만.. T0010 같은 코드가 화면 상에 보여진다면, 특정 린터 페이지의 설명화면으로 이동하는 링크를 심을 수 있다. kiyoon님의 dotfiles 참고
  • lua로 스크립팅이 가능한데, 활용할 수 있는 방향이 굉장히 다양하다.
    • 여기에서 설명한 기능들을 최대한 활용하여 스크립팅이 가능하다.
    • API 문서를 슥 훑어보았을때, "어? 설마 이게 되나?" 싶은 생각이 든다면, 여러분이 생각하는 것도 아마 가능할지도 모른다.

Wezterm, 써보자.

Wezterm을 설치한다면 설치 안내 페이지를 참고해서 설치하면 되는데, 여러분이 Wezterm을 설치했다면 당장은 검은 화면만 뜰지도 모른다.

Wezterm을 설치하고 나서, .config/wezterm/wezterm.lua 파일을 수정해야 하는데, 당장은 아래처럼 비어있을 것이다. 파일이 없다면 만들어두는 것이 좋다.

return {}

위의 코드에서 {}는 lua에서는 테이블(다른 언어로 치면, 딕셔너리/오브젝트 같은 것)이지만, wezterm 터미널의 configuration을 나타내는 테이블이다. 여기에 몇가지 추가사항을 넣어보겠다.

터미널 에뮬레이터를 설치했는데, 터미널 에뮬레이터를 설치했으면 가장 처음부터 하는게 무엇이겠는가? 바로, 폰트를 세팅하는 것이다. 터미널 환경에서 작업할때 폰트만큼 중요한게 또 없다.

local wezterm = require("wezterm")

return {
	font = wezterm.font_with_fallback({'Cascadia Code NF', 'NanumBarunGothic'}),
	font_size = 12.0,
	line_height = 1.2,
}

폰트를 가져다 쓸때는 위의 예시와 같이 font_with_fallback 함수를 이용해서 가져다 쓸 수 있고, 그 외에도 폰트 크기를 지정하거나 행간을 지정할 수도 있다. 공식페이지에서 보았듯이, 여러분의 취향에 따라 배경색 혹은 배경이미지도 지정할 수 있는데 여러분 나름대로의 기준이 있고 욕심이 난다면 한번 도전해보는 것도 나쁘지 않을 것 같다.

위의 코드를 복사 붙여넣고 편집하다보면 느낄 수 있겠지만, wezterm은 설정파일을 편집할때 Hot Reloading을 지원한다. 이 또한, 내가 가장 애정하는 기능 중 하나이다. 혹여나 wezterm 설정 파일을 수정했을때, 문법에 오류가 있거나 설정값을 잘못 지정했을 때, 새 창으로 어떤 부분에 오류가 있는지 친절하게 Alert도 띄워준다.

API 레퍼런스를 보기만 해도 스크립팅으로 기능을 확장할 수 있는 가능성이야 당연히 많긴 하겠지만, 처음 접하는 입장에서는 어떻게 커스터마이징할 지 파악하기 난해할 수 있다.

아래에서는 내가 어떻게 Wezterm을 커스터마이징을 하고 있는지 예시를 나열하는 것으로 글을 끝내겠다. Wezterm, 믿고 써보시라.

내가 Wezterm을 응용하는 방법

1. 반투명도 조정하기

Wezterm에서 터미널 색상을 설정할때, 배경색상의 반투명도를 지정할 수 있는 옵션이 있다. 나는 여기서 단축키를 입력했을때 반투명도를 동적으로 조절하고 싶었다.

반투명도를 상수로 둘 수는 있지만, 모니터 하나 짜리의 환경에서 작업한다면 브라우저를 뒤쪽에 두고 터미널 앱을 앞에 두는 식으로 작업을 많이 하게 된다. HMR(Hot Module Reloading)이 되는 개발환경이라면, 소스코드를 편집하고 화면에 즉각적으로 반영이 되는걸 기대할텐데 이걸 탭 스위칭하면서 확인하기는 굉장히 번거롭다. 온전히 작업을 유지하다가 잠깐 확인하고 싶을때 반투명도를 변경하면 되는데도 말이다.

Wezterm에서는 이벤트를 발신할 수 있고, 다른 프로세스에서 특정 이벤트를 수신했을때 어떤 동작을 할 것인지를 정의할 수 있다. Wezterm 터미널 옵션을 수정하는것도 여기에 포함될 수 있다. 특정 단축키를 입력하면, 어떤 이벤트를 발생시킬 수 있고, 그 이벤트로 인해서 화면의 반투명도를 조정할 수 있게 했다.

local default_opacity = 0.9

local keymaps = {}

-- SHIFT + CTRL + Z 키를 누르면 반투명도를 감소시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = "Z",
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'decrease-opacity',
  }
)

-- SHIFT + CTRL + X 키를 누르면 반투명도를 증가시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = 'X',
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'increase-opacity',
  }
)


-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('increase-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity
  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity + 0.1
  if opacity > 1.0 then
	opacity = 1.0
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('decrease-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity

  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity - 0.1
  if opacity < 0.3 then
	opacity = 0.3
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

return {
  keys = keymaps
}

2. 탭 이름 변경하기

대부분 터미널 에뮬레이터에서 탭 이름을 표시할때, 현재 탭에서 돌고 있는 프로세스의 이름을 명시할때가 많다. 그런데, 탭에 명시되어 있는 타이틀만 가지고는 각각의 탭이 어떤 역할을 하는 것인지 파악하기 어렵다. 어느 쪽이 서버를 띄우고 있는건지, 어느 쪽이 에디터 편집 화면인지, 어느 쪽이 LLM Agent를 돌리고 있는지 난해하다.

하지만, 아래처럼 wezterm.action.PromptInputLine API를 사용해서 프롬프트 입력을 받은 내용을 기반으로, 현재 활성화된 탭의 이름을 변경하는 식으로 인지부하를 줄일 수 있다.

local wezterm = require('wezterm')
local keymaps = {}

table.insert(
  keymaps,
  {
	key = '`',
	mods = 'CTRL',
	action = wezterm.action.PromptInputLine {
	  description = "Enter new name for tab",
	  action = wezterm.action_callback(function(window, _, line)
	    if line then
		  window:active_tab():set_title(line)
		end
	  end)
	}
  }
)

return {
  keys = keymaps
}

3. BEEP음 대신 화면이 번쩍이게 하기

Aider나 Claude Code 같은 LLM 에이전트가 작업을 수행 후 아예 작업이 끝났거나, 혹은 작업에 대한 승인을 요구할 때가 간혹 있다. LLM 에이전트가 작업을 수행하는걸 가만히 보고만 있을 순 없을 것이다.

Claude Code/Aider는 다행히도 응답을 하고 나서 추가적인 명령을 실행할 수 있는 옵션[2]을 제공해주는데, 거기에 간단하게 echo -ne '\007' 명령어를 넘겨줄 수 있다. 이 명령어는 엄청 어렵지는 않다. 터미널 앱에 내장된 시스템 벨 소리를 재생하는 명령어다.

작업 환경이 어디냐에 따라 다를 순 있겠지만, 작업 완료 여부를 음성으로 받기에는 물리적인 제약이 있을 수 있다. 아예 알림 센터를 이용한다고 치자. 업무 시간대에 방해금지 모드를 설정했다면 알림이 울리게 설정이 했더라도 시스템 제약상 묻힐 가능성도 있다.[3]

visual_bell 옵션을 활용하면, 터미널에 내장된 시스템 벨 소리를 울리는 대신 화면이 반짝이게 해서 다른 에이전트가 응답을 완료했다고 명시적으로 알림을 받을 수 있다. LLM 에이전트가 하는 일을 일일이 모니터링하지 않더라도, 다른 작업을 수행하는 중에 화면이 반짝이면 그때 확인하기만 하면 그만이다.

return {
	colors = {
		visual_bell = '#003355',
	},

	visual_bell = {
		fade_in_duration_ms  = 75,
		fade_out_duration_ms = 75,
		target               = 'BackgroundColor', -- 또는 'CursorColor'
	}
}

  1. Hashicorp 공동창업자인 Mitchell Hashimoto가 만든 터미널 에뮬레이터인데, OpenAI에서 Codex 시연할때 Ghostty 터미널로 시연한 바가 있다. ↩︎

  2. Aider는 notifications-command, Claude Code는 Hook이 있다 ↩︎

  3. 어떤 사람은 휴대전화를 진동모드로 해놓고, 텔레그램/디스코드로 알림쏴서 알림을 수신받는 방식을 쓰고 있는 것을 관찰한 바는 있다. ↩︎

@kodingwarrior@hackers.pub

이 글(그리고 후속작이 될 글들)은, 내 개발환경에서 자주 사용하고 있고 실제로도 애정하고 있는 도구에 대해서 소개하게 될 것 같다. 내가 어떤 환경에서 개발하고 있는지 궁금한 분들은 내 dotfiles 리포지토리를 참고해도 좋을 것 같다.

먼저, dotfiles란 무엇인가?

dotfiles라는 이름 자체만 보면 뭔가 대단해보일 것 같지만, dotfiles라는 이름 자체는 그냥 단순하다.

  • 어딘가의 가이드라인에서 지시하는대로 개발환경을 세팅하다보면, .zshrc/.bashrc 같은 것들을 마주하게 될 것이다.
  • git을 사용하고 있는 사람이라면, 어떤 diff 도구(delta, difft)를 사용할지, 어떤 alias 명령어를 등록할지 같은 것들을 명시하기 위해서 .gitconfig 같은 파일을 수정하게 될 때가 있다.
    • 물론, 이런것보다는 git config pull.rebase true 같은 명령어를 실행하는 경우가 더 많을 수 있다. 하지만, 이런 명령어를 실행하면 .gitconfig에도 그대로 기록이 된다.

위에서 언급한 예시를 보면 알 수 있겠듯이, 앞에 dot(.)이 붙어있는 설정파일이라면 dotfiles라고 할 수 있겠다. dotfiles 리포지토리를 만들어서 관리를 하는 이유는 무엇인가? 그것은 바로 어떤 환경에서든 .zshrc/.gitignore 파일 같은 것들을 동일하게 사용하여 작업의 흐름을 온전히 유지할 수 있다는 장점이 있기 때문이다. dotfiles를 git과 같은 버전관리 도구로 관리할 수 있고, github 같은 저장소에 올려놓을 수 있다면.... 어떤 개발환경으로 갈아타더라도 github에서 바로 내려받고, 각 설정파일들을 옮기면 그만이기 때문에 개발환경 설정하는데 드는 시간적 비용을 굉장히 아낄 수 있다.

dotfiles를 관리하는 방법들은 여러가지 있겠지만(symbolic link를 이용한다던가 등등), 개인적으로는 chezmoi를 권장하는 바이다. chezmoi는 dotfiles들을 버전관리할 수 있게 편의성을 제공해주는 CLI 도구이다.

  • 나도 chezmoi를 엄청 애용하고 있기 때문에, 내 dotfiles를 실제로 사용해보고 싶다면, chezmoi init https://github.com/malkoG/dotfiles.git 명령을 실행해보면 된다.

다시, 본론으로 Wezterm에 대해서 알아보자.

Wezterm은 Konsole/iTerm2/Gnome Terminal/Alacritty과 같은 터미널 에뮬레이터이며, Rust 기반으로 구축이 되어 있고, GPU 가속을 지원한다. 따라서, 렌더링 자체도 어느 정도는 빠른 편이다.

주변 사람들에게 한번 써보도록 권장하는 터미널 에뮬레이터가 세 가지 정도 있는데, Alacritty/Kitty 그리고 이 글에서 소개하는 Wezterm 정도 된다. 요즘은 Zig 기반으로 만들어진 Ghostty[1]도 추천할만 한 것 같다. 하지만, 이 글에서는 Wezterm을 소개하기로 했기 때문에, Wezterm 중심으로 소개하도록 하겠다.

Wezterm은 다음과 같은 특징을 가진다.

  • Linux, MacOS, 윈도우즈, FreeBSD 다양한 환경에서 돌아간다.
  • 한 윈도우를 여러개의 pane으로 쪼개서 분할하여 멀티플렉싱을 지원한다.(물론, 나는 ZelliJ/Tmux를 쓰기때문에 잘 이용하지는 않는 기능이다.
  • 마우스 지원이 잘 된다
  • 터미널에 표시되는 글자를 캡쳐해서 하이퍼링크로 치환이 가능하다.
    • 이건 나도 잘 이용하지는 않는 기능이지만.. T0010 같은 코드가 화면 상에 보여진다면, 특정 린터 페이지의 설명화면으로 이동하는 링크를 심을 수 있다. kiyoon님의 dotfiles 참고
  • lua로 스크립팅이 가능한데, 활용할 수 있는 방향이 굉장히 다양하다.
    • 여기에서 설명한 기능들을 최대한 활용하여 스크립팅이 가능하다.
    • API 문서를 슥 훑어보았을때, "어? 설마 이게 되나?" 싶은 생각이 든다면, 여러분이 생각하는 것도 아마 가능할지도 모른다.

Wezterm, 써보자.

Wezterm을 설치한다면 설치 안내 페이지를 참고해서 설치하면 되는데, 여러분이 Wezterm을 설치했다면 당장은 검은 화면만 뜰지도 모른다.

Wezterm을 설치하고 나서, .config/wezterm/wezterm.lua 파일을 수정해야 하는데, 당장은 아래처럼 비어있을 것이다. 파일이 없다면 만들어두는 것이 좋다.

return {}

위의 코드에서 {}는 lua에서는 테이블(다른 언어로 치면, 딕셔너리/오브젝트 같은 것)이지만, wezterm 터미널의 configuration을 나타내는 테이블이다. 여기에 몇가지 추가사항을 넣어보겠다.

터미널 에뮬레이터를 설치했는데, 터미널 에뮬레이터를 설치했으면 가장 처음부터 하는게 무엇이겠는가? 바로, 폰트를 세팅하는 것이다. 터미널 환경에서 작업할때 폰트만큼 중요한게 또 없다.

local wezterm = require("wezterm")

return {
	font = wezterm.font_with_fallback({'Cascadia Code NF', 'NanumBarunGothic'}),
	font_size = 12.0,
	line_height = 1.2,
}

폰트를 가져다 쓸때는 위의 예시와 같이 font_with_fallback 함수를 이용해서 가져다 쓸 수 있고, 그 외에도 폰트 크기를 지정하거나 행간을 지정할 수도 있다. 공식페이지에서 보았듯이, 여러분의 취향에 따라 배경색 혹은 배경이미지도 지정할 수 있는데 여러분 나름대로의 기준이 있고 욕심이 난다면 한번 도전해보는 것도 나쁘지 않을 것 같다.

위의 코드를 복사 붙여넣고 편집하다보면 느낄 수 있겠지만, wezterm은 설정파일을 편집할때 Hot Reloading을 지원한다. 이 또한, 내가 가장 애정하는 기능 중 하나이다. 혹여나 wezterm 설정 파일을 수정했을때, 문법에 오류가 있거나 설정값을 잘못 지정했을 때, 새 창으로 어떤 부분에 오류가 있는지 친절하게 Alert도 띄워준다.

API 레퍼런스를 보기만 해도 스크립팅으로 기능을 확장할 수 있는 가능성이야 당연히 많긴 하겠지만, 처음 접하는 입장에서는 어떻게 커스터마이징할 지 파악하기 난해할 수 있다.

아래에서는 내가 어떻게 Wezterm을 커스터마이징을 하고 있는지 예시를 나열하는 것으로 글을 끝내겠다. Wezterm, 믿고 써보시라.

내가 Wezterm을 응용하는 방법

1. 반투명도 조정하기

Wezterm에서 터미널 색상을 설정할때, 배경색상의 반투명도를 지정할 수 있는 옵션이 있다. 나는 여기서 단축키를 입력했을때 반투명도를 동적으로 조절하고 싶었다.

반투명도를 상수로 둘 수는 있지만, 모니터 하나 짜리의 환경에서 작업한다면 브라우저를 뒤쪽에 두고 터미널 앱을 앞에 두는 식으로 작업을 많이 하게 된다. HMR(Hot Module Reloading)이 되는 개발환경이라면, 소스코드를 편집하고 화면에 즉각적으로 반영이 되는걸 기대할텐데 이걸 탭 스위칭하면서 확인하기는 굉장히 번거롭다. 온전히 작업을 유지하다가 잠깐 확인하고 싶을때 반투명도를 변경하면 되는데도 말이다.

Wezterm에서는 이벤트를 발신할 수 있고, 다른 프로세스에서 특정 이벤트를 수신했을때 어떤 동작을 할 것인지를 정의할 수 있다. Wezterm 터미널 옵션을 수정하는것도 여기에 포함될 수 있다. 특정 단축키를 입력하면, 어떤 이벤트를 발생시킬 수 있고, 그 이벤트로 인해서 화면의 반투명도를 조정할 수 있게 했다.

local default_opacity = 0.9

local keymaps = {}

-- SHIFT + CTRL + Z 키를 누르면 반투명도를 감소시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = "Z",
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'decrease-opacity',
  }
)

-- SHIFT + CTRL + X 키를 누르면 반투명도를 증가시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = 'X',
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'increase-opacity',
  }
)


-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('increase-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity
  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity + 0.1
  if opacity > 1.0 then
	opacity = 1.0
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('decrease-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity

  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity - 0.1
  if opacity < 0.3 then
	opacity = 0.3
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

return {
  keys = keymaps
}

2. 탭 이름 변경하기

대부분 터미널 에뮬레이터에서 탭 이름을 표시할때, 현재 탭에서 돌고 있는 프로세스의 이름을 명시할때가 많다. 그런데, 탭에 명시되어 있는 타이틀만 가지고는 각각의 탭이 어떤 역할을 하는 것인지 파악하기 어렵다. 어느 쪽이 서버를 띄우고 있는건지, 어느 쪽이 에디터 편집 화면인지, 어느 쪽이 LLM Agent를 돌리고 있는지 난해하다.

하지만, 아래처럼 wezterm.action.PromptInputLine API를 사용해서 프롬프트 입력을 받은 내용을 기반으로, 현재 활성화된 탭의 이름을 변경하는 식으로 인지부하를 줄일 수 있다.

local wezterm = require('wezterm')
local keymaps = {}

table.insert(
  keymaps,
  {
	key = '`',
	mods = 'CTRL',
	action = wezterm.action.PromptInputLine {
	  description = "Enter new name for tab",
	  action = wezterm.action_callback(function(window, _, line)
	    if line then
		  window:active_tab():set_title(line)
		end
	  end)
	}
  }
)

return {
  keys = keymaps
}

3. BEEP음 대신 화면이 번쩍이게 하기

Aider나 Claude Code 같은 LLM 에이전트가 작업을 수행 후 아예 작업이 끝났거나, 혹은 작업에 대한 승인을 요구할 때가 간혹 있다. LLM 에이전트가 작업을 수행하는걸 가만히 보고만 있을 순 없을 것이다.

Claude Code/Aider는 다행히도 응답을 하고 나서 추가적인 명령을 실행할 수 있는 옵션[2]을 제공해주는데, 거기에 간단하게 echo -ne '\007' 명령어를 넘겨줄 수 있다. 이 명령어는 엄청 어렵지는 않다. 터미널 앱에 내장된 시스템 벨 소리를 재생하는 명령어다.

작업 환경이 어디냐에 따라 다를 순 있겠지만, 작업 완료 여부를 음성으로 받기에는 물리적인 제약이 있을 수 있다. 아예 알림 센터를 이용한다고 치자. 업무 시간대에 방해금지 모드를 설정했다면 알림이 울리게 설정이 했더라도 시스템 제약상 묻힐 가능성도 있다.[3]

visual_bell 옵션을 활용하면, 터미널에 내장된 시스템 벨 소리를 울리는 대신 화면이 반짝이게 해서 다른 에이전트가 응답을 완료했다고 명시적으로 알림을 받을 수 있다. LLM 에이전트가 하는 일을 일일이 모니터링하지 않더라도, 다른 작업을 수행하는 중에 화면이 반짝이면 그때 확인하기만 하면 그만이다.

return {
	colors = {
		visual_bell = '#003355',
	},

	visual_bell = {
		fade_in_duration_ms  = 75,
		fade_out_duration_ms = 75,
		target               = 'BackgroundColor', -- 또는 'CursorColor'
	}
}

  1. Hashicorp 공동창업자인 Mitchell Hashimoto가 만든 터미널 에뮬레이터인데, OpenAI에서 Codex 시연할때 Ghostty 터미널로 시연한 바가 있다. ↩︎

  2. Aider는 notifications-command, Claude Code는 Hook이 있다 ↩︎

  3. 어떤 사람은 휴대전화를 진동모드로 해놓고, 텔레그램/디스코드로 알림쏴서 알림을 수신받는 방식을 쓰고 있는 것을 관찰한 바는 있다. ↩︎

@kodingwarrior@hackers.pub

이 글(그리고 후속작이 될 글들)은, 내 개발환경에서 자주 사용하고 있고 실제로도 애정하고 있는 도구에 대해서 소개하게 될 것 같다. 내가 어떤 환경에서 개발하고 있는지 궁금한 분들은 내 dotfiles 리포지토리를 참고해도 좋을 것 같다.

먼저, dotfiles란 무엇인가?

dotfiles라는 이름 자체만 보면 뭔가 대단해보일 것 같지만, dotfiles라는 이름 자체는 그냥 단순하다.

  • 어딘가의 가이드라인에서 지시하는대로 개발환경을 세팅하다보면, .zshrc/.bashrc 같은 것들을 마주하게 될 것이다.
  • git을 사용하고 있는 사람이라면, 어떤 diff 도구(delta, difft)를 사용할지, 어떤 alias 명령어를 등록할지 같은 것들을 명시하기 위해서 .gitconfig 같은 파일을 수정하게 될 때가 있다.
    • 물론, 이런것보다는 git config pull.rebase true 같은 명령어를 실행하는 경우가 더 많을 수 있다. 하지만, 이런 명령어를 실행하면 .gitconfig에도 그대로 기록이 된다.

위에서 언급한 예시를 보면 알 수 있겠듯이, 앞에 dot(.)이 붙어있는 설정파일이라면 dotfiles라고 할 수 있겠다. dotfiles 리포지토리를 만들어서 관리를 하는 이유는 무엇인가? 그것은 바로 어떤 환경에서든 .zshrc/.gitignore 파일 같은 것들을 동일하게 사용하여 작업의 흐름을 온전히 유지할 수 있다는 장점이 있기 때문이다. dotfiles를 git과 같은 버전관리 도구로 관리할 수 있고, github 같은 저장소에 올려놓을 수 있다면.... 어떤 개발환경으로 갈아타더라도 github에서 바로 내려받고, 각 설정파일들을 옮기면 그만이기 때문에 개발환경 설정하는데 드는 시간적 비용을 굉장히 아낄 수 있다.

dotfiles를 관리하는 방법들은 여러가지 있겠지만(symbolic link를 이용한다던가 등등), 개인적으로는 chezmoi를 권장하는 바이다. chezmoi는 dotfiles들을 버전관리할 수 있게 편의성을 제공해주는 CLI 도구이다.

  • 나도 chezmoi를 엄청 애용하고 있기 때문에, 내 dotfiles를 실제로 사용해보고 싶다면, chezmoi init https://github.com/malkoG/dotfiles.git 명령을 실행해보면 된다.

다시, 본론으로 Wezterm에 대해서 알아보자.

Wezterm은 Konsole/iTerm2/Gnome Terminal/Alacritty과 같은 터미널 에뮬레이터이며, Rust 기반으로 구축이 되어 있고, GPU 가속을 지원한다. 따라서, 렌더링 자체도 어느 정도는 빠른 편이다.

주변 사람들에게 한번 써보도록 권장하는 터미널 에뮬레이터가 세 가지 정도 있는데, Alacritty/Kitty 그리고 이 글에서 소개하는 Wezterm 정도 된다. 요즘은 Zig 기반으로 만들어진 Ghostty[1]도 추천할만 한 것 같다. 하지만, 이 글에서는 Wezterm을 소개하기로 했기 때문에, Wezterm 중심으로 소개하도록 하겠다.

Wezterm은 다음과 같은 특징을 가진다.

  • Linux, MacOS, 윈도우즈, FreeBSD 다양한 환경에서 돌아간다.
  • 한 윈도우를 여러개의 pane으로 쪼개서 분할하여 멀티플렉싱을 지원한다.(물론, 나는 ZelliJ/Tmux를 쓰기때문에 잘 이용하지는 않는 기능이다.
  • 마우스 지원이 잘 된다
  • 터미널에 표시되는 글자를 캡쳐해서 하이퍼링크로 치환이 가능하다.
    • 이건 나도 잘 이용하지는 않는 기능이지만.. T0010 같은 코드가 화면 상에 보여진다면, 특정 린터 페이지의 설명화면으로 이동하는 링크를 심을 수 있다. kiyoon님의 dotfiles 참고
  • lua로 스크립팅이 가능한데, 활용할 수 있는 방향이 굉장히 다양하다.
    • 여기에서 설명한 기능들을 최대한 활용하여 스크립팅이 가능하다.
    • API 문서를 슥 훑어보았을때, "어? 설마 이게 되나?" 싶은 생각이 든다면, 여러분이 생각하는 것도 아마 가능할지도 모른다.

Wezterm, 써보자.

Wezterm을 설치한다면 설치 안내 페이지를 참고해서 설치하면 되는데, 여러분이 Wezterm을 설치했다면 당장은 검은 화면만 뜰지도 모른다.

Wezterm을 설치하고 나서, .config/wezterm/wezterm.lua 파일을 수정해야 하는데, 당장은 아래처럼 비어있을 것이다. 파일이 없다면 만들어두는 것이 좋다.

return {}

위의 코드에서 {}는 lua에서는 테이블(다른 언어로 치면, 딕셔너리/오브젝트 같은 것)이지만, wezterm 터미널의 configuration을 나타내는 테이블이다. 여기에 몇가지 추가사항을 넣어보겠다.

터미널 에뮬레이터를 설치했는데, 터미널 에뮬레이터를 설치했으면 가장 처음부터 하는게 무엇이겠는가? 바로, 폰트를 세팅하는 것이다. 터미널 환경에서 작업할때 폰트만큼 중요한게 또 없다.

local wezterm = require("wezterm")

return {
	font = wezterm.font_with_fallback({'Cascadia Code NF', 'NanumBarunGothic'}),
	font_size = 12.0,
	line_height = 1.2,
}

폰트를 가져다 쓸때는 위의 예시와 같이 font_with_fallback 함수를 이용해서 가져다 쓸 수 있고, 그 외에도 폰트 크기를 지정하거나 행간을 지정할 수도 있다. 공식페이지에서 보았듯이, 여러분의 취향에 따라 배경색 혹은 배경이미지도 지정할 수 있는데 여러분 나름대로의 기준이 있고 욕심이 난다면 한번 도전해보는 것도 나쁘지 않을 것 같다.

위의 코드를 복사 붙여넣고 편집하다보면 느낄 수 있겠지만, wezterm은 설정파일을 편집할때 Hot Reloading을 지원한다. 이 또한, 내가 가장 애정하는 기능 중 하나이다. 혹여나 wezterm 설정 파일을 수정했을때, 문법에 오류가 있거나 설정값을 잘못 지정했을 때, 새 창으로 어떤 부분에 오류가 있는지 친절하게 Alert도 띄워준다.

API 레퍼런스를 보기만 해도 스크립팅으로 기능을 확장할 수 있는 가능성이야 당연히 많긴 하겠지만, 처음 접하는 입장에서는 어떻게 커스터마이징할 지 파악하기 난해할 수 있다.

아래에서는 내가 어떻게 Wezterm을 커스터마이징을 하고 있는지 예시를 나열하는 것으로 글을 끝내겠다. Wezterm, 믿고 써보시라.

내가 Wezterm을 응용하는 방법

1. 반투명도 조정하기

Wezterm에서 터미널 색상을 설정할때, 배경색상의 반투명도를 지정할 수 있는 옵션이 있다. 나는 여기서 단축키를 입력했을때 반투명도를 동적으로 조절하고 싶었다.

반투명도를 상수로 둘 수는 있지만, 모니터 하나 짜리의 환경에서 작업한다면 브라우저를 뒤쪽에 두고 터미널 앱을 앞에 두는 식으로 작업을 많이 하게 된다. HMR(Hot Module Reloading)이 되는 개발환경이라면, 소스코드를 편집하고 화면에 즉각적으로 반영이 되는걸 기대할텐데 이걸 탭 스위칭하면서 확인하기는 굉장히 번거롭다. 온전히 작업을 유지하다가 잠깐 확인하고 싶을때 반투명도를 변경하면 되는데도 말이다.

Wezterm에서는 이벤트를 발신할 수 있고, 다른 프로세스에서 특정 이벤트를 수신했을때 어떤 동작을 할 것인지를 정의할 수 있다. Wezterm 터미널 옵션을 수정하는것도 여기에 포함될 수 있다. 특정 단축키를 입력하면, 어떤 이벤트를 발생시킬 수 있고, 그 이벤트로 인해서 화면의 반투명도를 조정할 수 있게 했다.

local default_opacity = 0.9

local keymaps = {}

-- SHIFT + CTRL + Z 키를 누르면 반투명도를 감소시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = "Z",
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'decrease-opacity',
  }
)

-- SHIFT + CTRL + X 키를 누르면 반투명도를 증가시키는 이벤트가 발생한다.
table.insert(
  keymaps,
  {
	key = 'X',
	mods = 'SHIFT|CTRL',
	action = wezterm.action.EmitEvent 'increase-opacity',
  }
)


-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('increase-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity
  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity + 0.1
  if opacity > 1.0 then
	opacity = 1.0
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

-- "increase-opacity" 이벤트를 수신했을때, 반투명도를 증가시키는 동작을 하도록 정의한다.
wezterm.on('decrease-opacity', function(window, pane)
  local overrides = window:get_config_overrides() or {}
  local opacity = overrides.window_background_opacity

  if opacity == nil then
	opacity = default_opacity
  end

  opacity = opacity - 0.1
  if opacity < 0.3 then
	opacity = 0.3
  end
  overrides.window_background_opacity = opacity

  window:set_config_overrides(overrides)
end)

return {
  keys = keymaps
}

2. 탭 이름 변경하기

대부분 터미널 에뮬레이터에서 탭 이름을 표시할때, 현재 탭에서 돌고 있는 프로세스의 이름을 명시할때가 많다. 그런데, 탭에 명시되어 있는 타이틀만 가지고는 각각의 탭이 어떤 역할을 하는 것인지 파악하기 어렵다. 어느 쪽이 서버를 띄우고 있는건지, 어느 쪽이 에디터 편집 화면인지, 어느 쪽이 LLM Agent를 돌리고 있는지 난해하다.

하지만, 아래처럼 wezterm.action.PromptInputLine API를 사용해서 프롬프트 입력을 받은 내용을 기반으로, 현재 활성화된 탭의 이름을 변경하는 식으로 인지부하를 줄일 수 있다.

local wezterm = require('wezterm')
local keymaps = {}

table.insert(
  keymaps,
  {
	key = '`',
	mods = 'CTRL',
	action = wezterm.action.PromptInputLine {
	  description = "Enter new name for tab",
	  action = wezterm.action_callback(function(window, _, line)
	    if line then
		  window:active_tab():set_title(line)
		end
	  end)
	}
  }
)

return {
  keys = keymaps
}

3. BEEP음 대신 화면이 번쩍이게 하기

Aider나 Claude Code 같은 LLM 에이전트가 작업을 수행 후 아예 작업이 끝났거나, 혹은 작업에 대한 승인을 요구할 때가 간혹 있다. LLM 에이전트가 작업을 수행하는걸 가만히 보고만 있을 순 없을 것이다.

Claude Code/Aider는 다행히도 응답을 하고 나서 추가적인 명령을 실행할 수 있는 옵션[2]을 제공해주는데, 거기에 간단하게 echo -ne '\007' 명령어를 넘겨줄 수 있다. 이 명령어는 엄청 어렵지는 않다. 터미널 앱에 내장된 시스템 벨 소리를 재생하는 명령어다.

작업 환경이 어디냐에 따라 다를 순 있겠지만, 작업 완료 여부를 음성으로 받기에는 물리적인 제약이 있을 수 있다. 아예 알림 센터를 이용한다고 치자. 업무 시간대에 방해금지 모드를 설정했다면 알림이 울리게 설정이 했더라도 시스템 제약상 묻힐 가능성도 있다.[3]

visual_bell 옵션을 활용하면, 터미널에 내장된 시스템 벨 소리를 울리는 대신 화면이 반짝이게 해서 다른 에이전트가 응답을 완료했다고 명시적으로 알림을 받을 수 있다. LLM 에이전트가 하는 일을 일일이 모니터링하지 않더라도, 다른 작업을 수행하는 중에 화면이 반짝이면 그때 확인하기만 하면 그만이다.

return {
	colors = {
		visual_bell = '#003355',
	},

	visual_bell = {
		fade_in_duration_ms  = 75,
		fade_out_duration_ms = 75,
		target               = 'BackgroundColor', -- 또는 'CursorColor'
	}
}

  1. Hashicorp 공동창업자인 Mitchell Hashimoto가 만든 터미널 에뮬레이터인데, OpenAI에서 Codex 시연할때 Ghostty 터미널로 시연한 바가 있다. ↩︎

  2. Aider는 notifications-command, Claude Code는 Hook이 있다 ↩︎

  3. 어떤 사람은 휴대전화를 진동모드로 해놓고, 텔레그램/디스코드로 알림쏴서 알림을 수신받는 방식을 쓰고 있는 것을 관찰한 바는 있다. ↩︎

🎉 Big thanks to @2chanhaeng for his first contribution to ! He implemented the new fedify webfinger command in PR #278, which allows isolated lookups for testing configurations. This addresses the need for developers to test WebFinger functionality without performing comprehensive object retrieval.

The contribution includes:

  • A new fedify webfinger <handle> command that accepts @user@domain format handles or URIs
  • Clean JSON output of WebFinger JRD results
  • Proper error handling for invalid handles and lookup failures
  • Complete integration with help text and usage examples

This was originally filed as issue #260 and marked as a good first issue—perfect for newcomers to learn the codebase structure while contributing meaningful functionality. The PR has been merged and will be included in the upcoming Fedify 1.8.0 release.

We appreciate all first-time contributors who help make Fedify better for the entire community. Welcome aboard, ChanHaeng!

github.com

fedify-dev/fedify

ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.

🎉 Big thanks to @2chanhaeng for his first contribution to ! He implemented the new fedify webfinger command in PR #278, which allows isolated lookups for testing configurations. This addresses the need for developers to test WebFinger functionality without performing comprehensive object retrieval.

The contribution includes:

  • A new fedify webfinger <handle> command that accepts @user@domain format handles or URIs
  • Clean JSON output of WebFinger JRD results
  • Proper error handling for invalid handles and lookup failures
  • Complete integration with help text and usage examples

This was originally filed as issue #260 and marked as a good first issue—perfect for newcomers to learn the codebase structure while contributing meaningful functionality. The PR has been merged and will be included in the upcoming Fedify 1.8.0 release.

We appreciate all first-time contributors who help make Fedify better for the entire community. Welcome aboard, ChanHaeng!

github.com

fedify-dev/fedify

ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.

🎉 Big thanks to @2chanhaeng for his first contribution to ! He implemented the new fedify webfinger command in PR #278, which allows isolated lookups for testing configurations. This addresses the need for developers to test WebFinger functionality without performing comprehensive object retrieval.

The contribution includes:

  • A new fedify webfinger <handle> command that accepts @user@domain format handles or URIs
  • Clean JSON output of WebFinger JRD results
  • Proper error handling for invalid handles and lookup failures
  • Complete integration with help text and usage examples

This was originally filed as issue #260 and marked as a good first issue—perfect for newcomers to learn the codebase structure while contributing meaningful functionality. The PR has been merged and will be included in the upcoming Fedify 1.8.0 release.

We appreciate all first-time contributors who help make Fedify better for the entire community. Welcome aboard, ChanHaeng!

github.com

fedify-dev/fedify

ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.

🎉 Big thanks to @2chanhaeng for his first contribution to ! He implemented the new fedify webfinger command in PR #278, which allows isolated lookups for testing configurations. This addresses the need for developers to test WebFinger functionality without performing comprehensive object retrieval.

The contribution includes:

  • A new fedify webfinger <handle> command that accepts @user@domain format handles or URIs
  • Clean JSON output of WebFinger JRD results
  • Proper error handling for invalid handles and lookup failures
  • Complete integration with help text and usage examples

This was originally filed as issue #260 and marked as a good first issue—perfect for newcomers to learn the codebase structure while contributing meaningful functionality. The PR has been merged and will be included in the upcoming Fedify 1.8.0 release.

We appreciate all first-time contributors who help make Fedify better for the entire community. Welcome aboard, ChanHaeng!

github.com

fedify-dev/fedify

ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.

🎉 Big thanks to @2chanhaeng for his first contribution to ! He implemented the new fedify webfinger command in PR #278, which allows isolated lookups for testing configurations. This addresses the need for developers to test WebFinger functionality without performing comprehensive object retrieval.

The contribution includes:

  • A new fedify webfinger <handle> command that accepts @user@domain format handles or URIs
  • Clean JSON output of WebFinger JRD results
  • Proper error handling for invalid handles and lookup failures
  • Complete integration with help text and usage examples

This was originally filed as issue #260 and marked as a good first issue—perfect for newcomers to learn the codebase structure while contributing meaningful functionality. The PR has been merged and will be included in the upcoming Fedify 1.8.0 release.

We appreciate all first-time contributors who help make Fedify better for the entire community. Welcome aboard, ChanHaeng!

github.com

fedify-dev/fedify

ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.

🎉 Big thanks to @2chanhaeng for his first contribution to ! He implemented the new fedify webfinger command in PR #278, which allows isolated lookups for testing configurations. This addresses the need for developers to test WebFinger functionality without performing comprehensive object retrieval.

The contribution includes:

  • A new fedify webfinger <handle> command that accepts @user@domain format handles or URIs
  • Clean JSON output of WebFinger JRD results
  • Proper error handling for invalid handles and lookup failures
  • Complete integration with help text and usage examples

This was originally filed as issue #260 and marked as a good first issue—perfect for newcomers to learn the codebase structure while contributing meaningful functionality. The PR has been merged and will be included in the upcoming Fedify 1.8.0 release.

We appreciate all first-time contributors who help make Fedify better for the entire community. Welcome aboard, ChanHaeng!

github.com

fedify-dev/fedify

ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.

🎉 Big thanks to @2chanhaeng for his first contribution to ! He implemented the new fedify webfinger command in PR #278, which allows isolated lookups for testing configurations. This addresses the need for developers to test WebFinger functionality without performing comprehensive object retrieval.

The contribution includes:

  • A new fedify webfinger <handle> command that accepts @user@domain format handles or URIs
  • Clean JSON output of WebFinger JRD results
  • Proper error handling for invalid handles and lookup failures
  • Complete integration with help text and usage examples

This was originally filed as issue #260 and marked as a good first issue—perfect for newcomers to learn the codebase structure while contributing meaningful functionality. The PR has been merged and will be included in the upcoming Fedify 1.8.0 release.

We appreciate all first-time contributors who help make Fedify better for the entire community. Welcome aboard, ChanHaeng!

github.com

fedify-dev/fedify

ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.

🎉 Big thanks to @2chanhaeng for his first contribution to ! He implemented the new fedify webfinger command in PR #278, which allows isolated lookups for testing configurations. This addresses the need for developers to test WebFinger functionality without performing comprehensive object retrieval.

The contribution includes:

  • A new fedify webfinger <handle> command that accepts @user@domain format handles or URIs
  • Clean JSON output of WebFinger JRD results
  • Proper error handling for invalid handles and lookup failures
  • Complete integration with help text and usage examples

This was originally filed as issue #260 and marked as a good first issue—perfect for newcomers to learn the codebase structure while contributing meaningful functionality. The PR has been merged and will be included in the upcoming Fedify 1.8.0 release.

We appreciate all first-time contributors who help make Fedify better for the entire community. Welcome aboard, ChanHaeng!

github.com

fedify-dev/fedify

ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.

@dweb@social.coop

⚡ Use the Internet Archive like a Hacker-Librarian! 🔮

Join a hands-on workshop where we’ll demystify the CLI and turn it into your super power for archiving, accessing, and uploading content to the @internetarchive.

Hosted by the @ZFAVClub, with the participation of @tommi, we’ll learn and share our experience using the awesome ia tool!

🗓️ Jul 3, 16:00 UTC (9 PDT, 18 CEST)
PARTICIPATION IS FREE

ℹ️ Info + registration: lu.ma/zv3blohp

Graphic with the title “Introduction to the Command Line Interface of Internet Archive”
ALT text

Graphic with the title “Introduction to the Command Line Interface of Internet Archive”

@readbeanicecream@mastodon.social

Mastodon on Toot TUI

There is nothing like a TUI (text user interface) to really help me focus. So when I stumbled on Toot TUI, I had to give it a try. Now, it is probably one of my favorite terminal applications.

readbeanicecream.surge.sh/2025

readbeanicecream.surge.sh

Mastodon on Toot TUI - ReadBeanIceCream

@paco@infosec.exchange

I am slowly oxidizing my unix CLI. A lot of people have made rust based versions of common unix utilities and some of them are REALLY good.

Like fd-find for doing essentially find . -name blah. And rg (ripgrep) which does grep -R but it's aware of git, files like pyc or .bak files, and it excludes them by default.

Now I have sd which is hopefully replacing the last thing I used perl for. I write perl -pi -e s/x/y/g a lot. Just doing a quick string replace inside a file. So sd can start doing that.

I'm also trying to get used to zellij instead of tmux and starship for modern prompt decorations like the kids do.

These kids, my friends, are welcome on my lawn.

rustutils.com

Rust Utilities

@jesuismonsieurb@framapiaf.org

Gens de Mastodon, j'ai une nouvelle fois besoin de votre aide.
Existerait-il un petite commande magique en bash qui permette de puiser dans un dossier contenant des images pour en faire un diaporama à partir d'elles, dans un ordre aléatoire ?

@jesuismonsieurb@framapiaf.org

Gens de Mastodon, j'ai une nouvelle fois besoin de votre aide.
Existerait-il un petite commande magique en bash qui permette de puiser dans un dossier contenant des images pour en faire un diaporama à partir d'elles, dans un ordre aléatoire ?

@grobi@defcon.social

Please Please stop the madness! Stop using Java-script for Unix/Linux tutorials !!

As an intensive Unix and Linux user, I love to use a command-line browser. As a rule, these are links, elinks or lynx for me.

So far, they have guided me to my goal at lightning speed, are resource-saving, require little bandwidth, do not distract with unnecessary bells and whistles and, above all, they work even if the X-system fails and I have to boot without a desktop !

For me, commandline or text-based browsers are part of the Linux fire brigade and are the salvation of many users in need. But it doesn't help at all if manuals, help pages and other tutorials use Java-script!

In the example below, you can see a search query in the links browser with the parameters: "Linux close all windows commandline"

Of the 10 websites shown in the search results, 7 (!) could not be displayed at all because of Javascript, 2 did not really correspond to the parameters and only one (the github page) had a corresponding readable answer.

Does that make sense?
Absolutely not!!

Therefore, an anxious request:

Dear Tutorial Content Creators, Dear OS Developers please stop using Javascript or at least provide a plain text or html option, PLEASE PLEASE PLEASE PLEASE PLEASE

@hugovk@mastodon.social · Reply to Hugo van Kemenade

Just released! 🚀

Enabled colour help for Python 3.14:

🎨 em-keyboard 5.2.0
🎨 linkotron 0.5.0
🎨 norwegianblue 0.22.0
🎨 pepotron 1.5.0
🎨 pypistats 1.10.0

Also added Python 3.15 to pepotron: `pep 3.15` opens PEP 790.

Colour output of `linkotron --help` and `em --help`.
ALT text

Colour output of `linkotron --help` and `em --help`.

Colour output of `pep --help`.
ALT text

Colour output of `pep --help`.

Colour output of `pypistats python_minor --help`.
ALT text

Colour output of `pypistats python_minor --help`.

Colour output of `eol --help`.
ALT text

Colour output of `eol --help`.

@lavxnews@ioc.exchange

Introducing kdlfmt: The Essential CLI Tool for KDL Document Formatting

The new kdlfmt CLI tool simplifies the formatting and validation of KDL (Kotlin Data Language) documents, making it an indispensable utility for developers working with this emerging data format. Buil...

news.lavx.hu/article/introduci

Introducing kdlfmt: The Essential CLI Tool for KDL Document Formatting
ALT text

Introducing kdlfmt: The Essential CLI Tool for KDL Document Formatting

@toxi@mastodon.thi.ng

Just pushed a new version of thi.ng/block-fs, now with additional multi-command CLI tooling to convert & bundle a local file system tree into a single block-based binary blob (e.g. for bundling assets, or distributing a virtual filesystem as part of a web app, or for snapshot testing, or as bridge for WASM interop etc.)

Also new, the main API now includes a `.readAsObjectURL()` method to wrap files as URLs to binary blobs with associated MIME types, thereby making it trivial to use the virtual filesystem for sourcing stored images and other assets for direct use in the browser...

(Ps. For more context see other recent announcement: mastodon.thi.ng/@toxi/11426498)

Screenshot excerpt from the project readme (link in post) containing information about the CLI wrapper, as well as example usage (here to convert/bundle as filesystem tree)
ALT text

Screenshot excerpt from the project readme (link in post) containing information about the CLI wrapper, as well as example usage (here to convert/bundle as filesystem tree)

Screenshot excerpt from the project readme (link in post) containing information about the CLI wrapper, as well as example usage (here to list contents of an already bundled filesystem)
ALT text

Screenshot excerpt from the project readme (link in post) containing information about the CLI wrapper, as well as example usage (here to list contents of an already bundled filesystem)

@epilys@chaos.social

Stupid-but-works tip on how to add inline documentation comments for multi-line shell commands in scripts: Combine command substitution with grave accents "`" and the do-nothing built-in command ":":

```shell
% ls \
> -h `: this is a comment` \
> -a `: this is another comment` \
> -t `: more commentssss`
```

@epilys@chaos.social

Stupid-but-works tip on how to add inline documentation comments for multi-line shell commands in scripts: Combine command substitution with grave accents "`" and the do-nothing built-in command ":":

```shell
% ls \
> -h `: this is a comment` \
> -a `: this is another comment` \
> -t `: more commentssss`
```

@hugovk@mastodon.social · Reply to Hugo van Kemenade

Just released: pypistats 1.9.0 🚀

pypistats is CLI to show download stats from PyPI

pypi.org/project/pypistats/1.9

* Replace deprecated classifier with licence expression (PEP 639)
* Remove GitHub attestation, uses PyPI attestations instead (PEP 740)
* Add input validation for total and fix --monthly with no mirror
* Update docs for recent command

Terminal output of running "pypistats python_minor pillow", showing a table of Python versions sorted by how many downloads each is responsible for.
ALT text

Terminal output of running "pypistats python_minor pillow", showing a table of Python versions sorted by how many downloads each is responsible for.

@scy@chaos.social

`lowdown -tterm` produces pretty nice rendering in the .

kristaps.bsd.lv/lowdown/

Screenshot of an article talking about tab-completion for bash aliases, rendered in plain text. Monospace parts are rendered blue & bold, links are yellow and bold and followed by the URL they're linking to in green and underlined. Code blocks are indented, bold, and with a blue line to their left.
ALT text

Screenshot of an article talking about tab-completion for bash aliases, rendered in plain text. Monospace parts are rendered blue & bold, links are yellow and bold and followed by the URL they're linking to in green and underlined. Code blocks are indented, bold, and with a blue line to their left.

@freespiritlinux69@fedi.at

Schönen guten Morgen,

Ich habe mich entschieden, ein kleines Tutorial über CLI-Befehle unter Linux zu erstellen.
CLI steht für Command Line Interface, was auf Deutsch als Befehlszeilenschnittstelle bezeichnet wird. Es handelt sich um eine textbasierte Benutzeroberfläche, die es Benutzern ermöglicht, mit einem Computer oder einem Betriebssystem zu interagieren, indem sie Befehle in Form von Text eingeben.
In Linux kann das sogenannte Terminal zur Eingabe von CLI-Befehlen verwendet werden.

Bitte teilt das mit eurer , damit mehr Menschen darauf aufmerksam werden. Dieses Tutorial richtet sich an Anfänger, die das Terminal unter Linux nicht als furchterregendes Monster betrachten, sondern effizient damit arbeiten möchten.

DANKESCHÖN

Der Link führt direkt zum Thread mit dem Tutorial und kann bei Bedarf gespeichert werden.

fedi.at/@freespiritlinux69/114

@tuist@fosstodon.org
@tuist@fosstodon.org
@artemissian@fosstodon.org
@artemissian@fosstodon.org
@hyalinesystems@mastodon.social
@ploum@mamot.fr

If you are a Unix nerd and wish you spend less time using your mouse while watching flashy colors, I recommend that you give Offpunk a try:

offpunk.net/

I’m trying to make it easier to get started with offpunk. Feedbacks and discussions are welcome on the mailing-list :

lists.sr.ht/~lioploum/offpunk-

or on the fediverse, using the hashtag. Or on your blog. That would be awesome to read blog posts about people using offpunk

lists.sr.ht

~lioploum/offpunk-users archives — lists.sr.ht

@ploum@mamot.fr

If you are a Unix nerd and wish you spend less time using your mouse while watching flashy colors, I recommend that you give Offpunk a try:

offpunk.net/

I’m trying to make it easier to get started with offpunk. Feedbacks and discussions are welcome on the mailing-list :

lists.sr.ht/~lioploum/offpunk-

or on the fediverse, using the hashtag. Or on your blog. That would be awesome to read blog posts about people using offpunk

lists.sr.ht

~lioploum/offpunk-users archives — lists.sr.ht

@hongminhee@hollo.social
@hongminhee@hollo.social
@blinry@chaos.social

I'm looking for a command line tool that allows watching a video file together on two computers, with synchronized play/pause, like these "watch party" sites, but inside a local network.

In the simplest case, it could be a Bash command streaming/decoding a local file at the same time, and another person could receive that stream…? Has anyone seen something like that?

@blinry@chaos.social

I'm looking for a command line tool that allows watching a video file together on two computers, with synchronized play/pause, like these "watch party" sites, but inside a local network.

In the simplest case, it could be a Bash command streaming/decoding a local file at the same time, and another person could receive that stream…? Has anyone seen something like that?

@ploum@mamot.fr

Released Offpunk 2.5 which add custom "aliases" and improve compatibility with and version < 3.11

What is Offpunk?

offpunk.net/whatisoffpunk.html

You are welcome to discuss and ask questions on the offpunk-users list:

lists.sr.ht/~lioploum/offpunk-

If you are familiar with python development, join the offpunk-devel list to help intregrate offpunk and unmerdify, a new library developed by @vjousse :

lists.sr.ht/~lioploum/offpunk-

lists.sr.ht

Integrating `unmerdify` to allow custom HTML parsing rules — sourcehut lists

@ploum@mamot.fr

Released Offpunk 2.5 which add custom "aliases" and improve compatibility with and version < 3.11

What is Offpunk?

offpunk.net/whatisoffpunk.html

You are welcome to discuss and ask questions on the offpunk-users list:

lists.sr.ht/~lioploum/offpunk-

If you are familiar with python development, join the offpunk-devel list to help intregrate offpunk and unmerdify, a new library developed by @vjousse :

lists.sr.ht/~lioploum/offpunk-

lists.sr.ht

Integrating `unmerdify` to allow custom HTML parsing rules — sourcehut lists

@WeirdWriter@caneandable.social

So, I am giving a try. It’s a command line utility that will allow you to copy things from one cloud storage to the other with ease, sync one way or buy directionally, and Mount cloud storage as virtual drives on your machine so you can mount things like Google Drive, iCloud Drive, and even OneDrive without using any of their bloated and inaccessible software. Of course, the first thing I tried to do with it, it’s not capable of Yet. I tried to copy my writing from an off-line hard drive to three different cloud services with one command. That’s not possible as of yet, but I would still highly recommend this tool even if I’m sure I’m not utilizing it to its full glory as of yet. , ,

@qiita@rss-mstdn.studiofreesia.com
@sramsay@hcommons.social

I'm pleased to present a new blog post -- "Beautiful Documents with Groff (Part II)" stephenramsay.net/posts/groff- -- only a year-and-a-half after "Beautiful Documents with Groff (Part I)" stephenramsay.net/posts/groff-

Of interest, perhaps, to users of and/or , cultists, and digital document nerds. Discusses , , , and even (though not the kind that makes you sneeze).

stephenramsay.net

Beautiful Documents with Groff (Part I)

@levibeach@merveilles.town · Reply to Levi Beach

So I guess it's happening and I'm really excited to explore all the possibilities. Using the Node binding for Rust's Web Audio API to create sound, which means I can do stuff like impulse response to model spaces!

Two command line windows, one running Orca, the other running something called "HORSE SYNTH". Extremely basic controls for wave shape and ADSR are visible.
ALT text

Two command line windows, one running Orca, the other running something called "HORSE SYNTH". Extremely basic controls for wave shape and ADSR are visible.

@hugovk@mastodon.social · Reply to Hugo van Kemenade

Just released: blurb 1.3.0 🚀

blurb is the CLI we use for managing CPython's news/changelog entries.

🗞️ Add support for Python 3.13

🗞️ Drop support for Python 3.8

🗞️ Generate digital attestations for PyPI (PEP 740)

🗞️ Allow running blurb test from blurb-* directories by

🗞️ Add version subcommand

🗞️ Generate __version__ at build to avoid slow importlib.metadata

pypi.org/project/blurb/1.3.0/

@hugovk@mastodon.social · Reply to Hugo van Kemenade

Just released: norwegianblue 0.19.0 🚀

🦜 Drop support for Python 3.8

🦜 Generate digital attestations for PyPI (PEP 740)

🦜 Test with tox-uv

🦜 Lint with pre-commit-uv

pypi.org/project/norwegianblue

norwegianblue is a CLI to show EOLs from endoflife.date

A CLI call of "eol python" showing a coloured table of each Python feature release, its latest x.y.z version and date, and the dates of when it enters security-only and EOL.
ALT text

A CLI call of "eol python" showing a coloured table of each Python feature release, its latest x.y.z version and date, and the dates of when it enters security-only and EOL.

@hugovk@mastodon.social · Reply to Hugo van Kemenade

Just released: pepotron 1.3.0 🚀

🔩 Generate digital attestations for PyPI (PEP 740)

🔩 Drop support for Python 3.8

🔩 Generate __version__ at build to avoid slow importlib.metadata

🔩 Test on CI with uv

pypi.org/project/pepotron/1.3.

Pepotron is a CLI for opening PEPs in your browser. For example, try:

$ pep 8

$ pep 3.14

$ pep dead batteries

$ pep calendar

@hugovk@mastodon.social · Reply to Hugo van Kemenade

Just released: pypistats 1.7.0 🚀

📈 Generate digital attestations for PyPI (PEP 740)

📉 Drop support for EOL Python 3.8

📈 Generate __version__ at build to avoid slow importlib.metadata

pypi.org/project/pypistats/1.7

Example use, showing an ASCII chart:

❯ pypistats python_minor pillow --last-month
┌──────────┬─────────┬─────────────┐
│ category │ percent │   downloads │
├──────────┼─────────┼─────────────┤
│ 3.10     │  16.80% │  20,297,555 │
│ 3.11     │  15.78% │  19,062,442 │
│ 3.7      │  14.86% │  17,958,611 │
│ 3.9      │  13.68% │  16,530,171 │
│ 3.8      │  12.32% │  14,887,891 │
│ 3.6      │  11.73% │  14,169,137 │
│ 3.12     │   9.56% │  11,548,054 │
│ null     │   4.73% │   5,716,677 │
│ 2.7      │   0.50% │     598,393 │
│ 3.13     │   0.03% │      34,987 │
│ 3.5      │   0.02% │      20,237 │
│ 3.4      │   0.00% │         817 │
│ 3.14     │   0.00% │         232 │
│ 3.3      │   0.00% │          14 │
│ 3.1      │   0.00% │           5 │
│ 3.2      │   0.00% │           2 │
│ Total    │         │ 120,825,225 │
└──────────┴─────────┴─────────────┘

Date range: 2024-09-01 - 2024-09-30
ALT text

Example use, showing an ASCII chart: ❯ pypistats python_minor pillow --last-month ┌──────────┬─────────┬─────────────┐ │ category │ percent │ downloads │ ├──────────┼─────────┼─────────────┤ │ 3.10 │ 16.80% │ 20,297,555 │ │ 3.11 │ 15.78% │ 19,062,442 │ │ 3.7 │ 14.86% │ 17,958,611 │ │ 3.9 │ 13.68% │ 16,530,171 │ │ 3.8 │ 12.32% │ 14,887,891 │ │ 3.6 │ 11.73% │ 14,169,137 │ │ 3.12 │ 9.56% │ 11,548,054 │ │ null │ 4.73% │ 5,716,677 │ │ 2.7 │ 0.50% │ 598,393 │ │ 3.13 │ 0.03% │ 34,987 │ │ 3.5 │ 0.02% │ 20,237 │ │ 3.4 │ 0.00% │ 817 │ │ 3.14 │ 0.00% │ 232 │ │ 3.3 │ 0.00% │ 14 │ │ 3.1 │ 0.00% │ 5 │ │ 3.2 │ 0.00% │ 2 │ │ Total │ │ 120,825,225 │ └──────────┴─────────┴─────────────┘ Date range: 2024-09-01 - 2024-09-30

@whalecoiner@indieweb.social

Does anyone have a recommendation for a CLI boilerplate text file creator? I need something to help with using my personal website better. I’m thinking of something where I can type “<app> new post” or “<app> new note” and a markdown file of appropriate frontmatter stubs is created, in a predefined directory. It’d have to have some kind of template system available. Know of anything?

@OS1337@infosec.space

For everyone wanting to test out OS/1337 there's good news:

You can just clone the repo or pull it as :
github.com/OS-1337/OS1337

and then just run ./scripts/build.sh

and within a few mins it'll spit out a bootable image in /build/0.CORE/ to put on a 3,5" FDD or run in a VM [may it be or ]...

Thanks to @SweetAIBelle for the generous contributions!

@servio@libretics.org

Presentación

Imagen del Ñu la mascota oficial del movimiento del Software Libre.

: un lugar de encuentro, debate, investigación, desarrollo y difusión acerca de los usos sociales de la tecnología.

Contacto:

Web https://www.libretics.org

Radio https://libretics.org/radio

: podcast.libretics.org/

Grupo xmpp:hacklab-libretics@salas.gnlug…

Grupo : i.delta.chat/#FF36E74BCB6E7C00

Blog: http://3puc73jz3pbflplwe7y5hkopdoq…

Me gusta mucho el concepto de la internet pequeña: 1 persona = 1 servidor = 1 página web. 🧑‍💻🧰🐃🐧🇪🇨

Imagen del Ñu la mascota oficial del movimiento del Software Libre.
ALT text

Imagen del Ñu la mascota oficial del movimiento del Software Libre.

@scy@chaos.social

`lowdown -tterm` produces pretty nice rendering in the .

kristaps.bsd.lv/lowdown/

Screenshot of an article talking about tab-completion for bash aliases, rendered in plain text. Monospace parts are rendered blue & bold, links are yellow and bold and followed by the URL they're linking to in green and underlined. Code blocks are indented, bold, and with a blue line to their left.
ALT text

Screenshot of an article talking about tab-completion for bash aliases, rendered in plain text. Monospace parts are rendered blue & bold, links are yellow and bold and followed by the URL they're linking to in green and underlined. Code blocks are indented, bold, and with a blue line to their left.

@zornslemmon@mathstodon.xyz

Hello! I'm Dave, a non-academic research physicist.

Likes: , , data visualization, , , open source software, , , command line interfaces, \(\LaTeX\ \), and using latin shortcuts, *e.g., *i.e.*, *e.g.*, *etc.*, *etc.*

Dislikes: seeing Q.E.D. used where Q.E.F. is needed.

I get anxiety making up hashtags and I obsess over maxing out my type in boxes when shown a decrementing character count.

@vesperto@mastodon.social

Considering i may have forgotten to click submit on that last , here are a bunch of random tags to complement my bio: , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , EOL.

@sgued@pouet.chapril.org

🇫🇷 Étudiant, passionné de logiciels libres. J'ai créé peertube-viewer, un petit outil en ligne de commande pour naviguer les vidéos , dans le même esprit que youtube-viewer

🇬🇧 Student, passionate by . I created peertube-viewer, a small tool to browse peertube instances, quite similar to youtube-viewer.