From 8abe8f985846efc178f4dc650f0a8f30afa256ad Mon Sep 17 00:00:00 2001 From: administrator Date: Thu, 1 Oct 2026 18:03:49 +0000 Subject: [PATCH 1/3] chore(config): save the btop config captured on 2026-10-01 Byte-for-byte copy of the btop.conf btop 1.4.7 wrote on a clean quit (sha256 9d48d3c289346e4e9f55bc9e8888369ed155268448857de5bb4a7f8326ec49e0). --- config/btop.conf | 254 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 config/btop.conf diff --git a/config/btop.conf b/config/btop.conf new file mode 100644 index 0000000..45e5757 --- /dev/null +++ b/config/btop.conf @@ -0,0 +1,254 @@ +#? Config file for btop v.1.4.7 + +#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. +#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" +color_theme = "Default" + +#* If the theme set background should be shown, set to False if you want terminal background transparency. +theme_background = false + +#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. +truecolor = true + +#* Set to true to force tty mode regardless if a real tty has been detected or not. +#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. +force_tty = false + +#* Option to disable presets. Either the default preset, custom presets, or all presets. +#* "Off" All presets are enabled. +#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled. +disable_presets = "Off" + +#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. +#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. +#* Use whitespace " " as separator between different presets. +#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" +presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" + +#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. +#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. +vim_keys = false + +#* Disable all mouse events. +disable_mouse = false + +#* Rounded corners on boxes, is ignored if TTY mode is ON. +rounded_corners = true + +#* Use terminal synchronized output sequences to reduce flickering on supported terminals. +terminal_sync = true + +#* Default symbols to use for graph creation, "braille", "block" or "tty". +#* "braille" offers the highest resolution but might not be included in all fonts. +#* "block" has half the resolution of braille but uses more common characters. +#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY. +#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view. +graph_symbol = "braille" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_cpu = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_mem = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_net = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_proc = "default" + +#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace. +shown_boxes = "cpu mem net proc" + +#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. +update_ms = 100 + +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct", +#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly. +proc_sorting = "cpu direct" + +#* Reverse sorting order, True or False. +proc_reversed = false + +#* Show processes as a tree. +proc_tree = false + +#* Use the cpu graph colors in the process list. +proc_colors = true + +#* Use a darkening gradient in the process list. +proc_gradient = true + +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power. +proc_per_core = false + +#* Show process memory as bytes instead of percent. +proc_mem_bytes = true + +#* Show cpu graph for each process. +proc_cpu_graphs = true + +#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate) +proc_info_smaps = false + +#* Show proc box on left side of screen instead of right. +proc_left = false + +#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop). +proc_filter_kernel = false + +#* Should the process list follow the selected process when detailed view is open. +proc_follow_detailed = true + +#* In tree-view, always accumulate child process resources in the parent process. +proc_aggregate = false + +#* In tree-view, auto-collapse processes with this many or more direct children when +#* entering tree mode. 0 to disable. Useful for collapsing multi-process apps like browsers. +proc_tree_auto_collapse = 0 + +#* Should cpu and memory usage display be preserved for dead processes when paused. +keep_dead_proc_usage = false + +#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_upper = "Auto" + +#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_lower = "Auto" + +#* Toggles if the lower CPU graph should be inverted. +cpu_invert_lower = true + +#* Set to True to completely disable the lower CPU graph. +cpu_single_graph = false + +#* Show cpu box at bottom of screen instead of top. +cpu_bottom = false + +#* Shows the system uptime in the CPU box. +show_uptime = true + +#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo. +show_cpu_watts = true + +#* Show cpu temperature. +check_temp = true + +#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. +cpu_sensor = "Auto" + +#* Show temperatures for cpu cores also if check_temp is True and sensors has been found. +show_coretemp = true + +#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core. +#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine. +#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries. +#* Example: "4:0 5:1 6:3" +cpu_core_map = "" + +#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". +temp_scale = "celsius" + +#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. +base_10_sizes = false + +#* Show CPU frequency. +show_cpu_freq = true + +#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average". +freq_mode = "first" + +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable. +#* Special formatting: /host = hostname | /user = username | /uptime = system uptime +clock_format = "%X" + +#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. +background_update = true + +#* Custom cpu model name, empty string to disable. +custom_cpu_name = "" + +#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ". +#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disks_filter="/boot /home/user", disks_filter="exclude=/boot /home/user" +disks_filter = "" + +#* Show graphs instead of meters for memory values. +mem_graphs = true + +#* Show mem box below net box instead of above. +mem_below_net = false + +#* Count ZFS ARC in cached and available memory. +zfs_arc_cached = true + +#* If swap memory should be shown in memory box. +show_swap = true + +#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. +swap_disk = true + +#* If mem box should be split to also show disks info. +show_disks = true + +#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. +only_physical = true + +#* Read disks list from /etc/fstab. This also disables only_physical. +use_fstab = true + +#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool) +zfs_hide_datasets = false + +#* Set to true to show available disk space for privileged users. +disk_free_priv = false + +#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. +show_io_stat = true + +#* Toggles io mode for disks, showing big graphs for disk read/write speeds. +io_mode = false + +#* Set to True to show combined read/write io graphs in io mode. +io_graph_combined = false + +#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ". +#* Example: "/mnt/media:100 /:20 /boot:1". +io_graph_speeds = "" + +#* Swap the positions of the upload and download speed graphs. When true, upload will be on top. +swap_upload_download = false + +#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. +net_download = 100 + +net_upload = 100 + +#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. +net_auto = true + +#* Sync the auto scaling for download and upload to whichever currently has the highest scale. +net_sync = true + +#* Starts with the Network Interface specified here. +net_iface = "" + +#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes. +base_10_bitrate = "Auto" + +#* Show battery stats in top right if battery is present. +show_battery = true + +#* Which battery to use if multiple are present. "Auto" for auto detection. +selected_battery = "Auto" + +#* Show power stats of battery next to charge indicator. +show_battery_watts = true + +#* Set loglevel for "~/.local/state/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". +#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. +log_level = "WARNING" + +#* Automatically save current settings to config file on exit. +save_config_on_exit = true From aad4be2b629243b4a55733808374e504739832b1 Mon Sep 17 00:00:00 2001 From: administrator Date: Thu, 1 Oct 2026 18:10:23 +0000 Subject: [PATCH 2/3] feat(btop): install btop and fit it to the pane so it loads on a phone btop shows only "Terminal size too small" in a pane smaller than its shown boxes need: 80x24 for all four boxes in btop 1.4 (cpu 60 columns wide, proc 44 beside the 36 of mem and net). A Herdr phone pane is about 50 columns, so btop never loaded there. Channel: btop comes from the latest upstream GitHub release (aristocratos/btop), not the snap. scripts/install_tools.py installs the static musl build as a core tool, verified against the GitHub release-asset SHA-256, the same path herdr, gh and treehouse use (docs/architecture.md rule 1). The snap is a third-party repack (publisher kz6fittycent, not upstream) with no published checksum the installer can verify, and snapd is not available in the container image. The installer links the binary as ~/.local/bin/btop-bin. It reads ~/.config/btop/btop.conf, so the config is installed there instead of at the snap path. Config change against the capture (sha256 9d48d3c2...): the previous commit is the capture byte-for-byte. This commit changes one line: line 26, presets, gains " proc:0:default", which defines preset 4 (processes only). btop 1.4.7 does not know the captured proc_tree_auto_collapse = 0 line (the snap was built from a newer upstream commit), so it ignores the line and drops it when it rewrites the file on exit. Every apply restores it. Mechanism: maintenance/btop.sh (plain sh, about 20 lines), installed as ~/.local/bin/btop, reads stty size. It starts btop with preset 0 (all boxes) in a pane of at least 80x24, and with preset 4 otherwise. The threshold is btop's real minimum for all four boxes, measured on real btop; at 50 columns only proc alone (44x16) or mem+net fits, and preset 4 keeps the saved proc_sorting = "cpu direct". A preset in btop.conf is the only way to choose boxes from the command line (-p). The launcher runs btop in the background and traps WINCH. When a live resize crosses 80x24, it sends TERM (btop exits cleanly) and starts btop again with the other preset. When btop quits, the launcher exits with btop's status. It always passes -p, because btop saves the last preset's shown_boxes on exit; without -p, a phone session would leave the desktop pane showing only proc. btop's stdin is a copy of the original stdin, not /dev/tty: with a /dev/tty stdin btop assumes a Linux console and drops to 16-color tty mode. ansible/tasks/herdr.yml installs the launcher and config on every apply; verify.yml checks they exist. Proof, real btop 1.4.7 in throwaway tmux sessions: 50x40 shows preset 4; a live resize to 177x66 shows the full layout; back to 50x40 and wide again switch correctly; quitting exits 0. 80x24 shows the full layout, 79x30 and 120x23 show preset 4. Quitting in phone mode and then starting wide shows the full layout. Five rapid resize cycles end on the correct preset with no leftover processes. --- ansible/group_vars/all.yml | 6 +++++- ansible/tasks/herdr.yml | 32 ++++++++++++++++++++++++++++++++ ansible/tasks/tools.yml | 2 +- ansible/tasks/verify.yml | 3 +++ config/btop.conf | 2 +- docs/dependencies.md | 4 ++-- docs/herdr.md | 8 ++++++++ maintenance/btop.sh | 23 +++++++++++++++++++++++ scripts/install_tools.py | 10 +++++++++- tests/container-smoke.sh | 4 ++-- tests/test_install_tools.py | 15 +++++++++++++-- 11 files changed, 99 insertions(+), 10 deletions(-) create mode 100755 maintenance/btop.sh diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 14663da..af78c93 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -177,7 +177,7 @@ factory_desktop_packages: - iproute2 # --- Tool installer --------------------------------------------------------- -factory_core_tools: [herdr, node, bun, uv] +factory_core_tools: [herdr, node, bun, uv, btop] # The installer flags that choose which tools a run covers. tasks/preflight.yml # resolves exactly this set (plus the sources Ansible installs itself) and # tasks/tools.yml installs it, so a host never resolves, and never fails on, a @@ -210,6 +210,10 @@ factory_no_mistakes_omp_overlay: "{{ factory_cfg.home }}/.no-mistakes/omp-config # Stdlib-only reporter that feeds the Spaces sidebar layout, run by herdr-spaces.timer. factory_herdr_spaces_source: "{{ code_factory_repo }}/maintenance/herdr-spaces.py" factory_herdr_spaces_script: "{{ factory_local_bin }}/herdr-spaces.py" +# btop: ~/.local/bin/btop is a launcher that picks a preset of the saved config +# by pane size, so it also loads in a phone's 50-column pane (docs/herdr.md#btop). +factory_btop_launcher_source: "{{ code_factory_repo }}/maintenance/btop.sh" +factory_btop_config: "{{ factory_cfg.home }}/.config/btop/btop.conf" # --- Browser pruning -------------------------------------------------------- # Flags confirmed by Main (owner of maintenance/chrome-autoprune.py): without diff --git a/ansible/tasks/herdr.yml b/ansible/tasks/herdr.yml index 0c6b02e..3d44847 100644 --- a/ansible/tasks/herdr.yml +++ b/ansible/tasks/herdr.yml @@ -105,6 +105,38 @@ remote_src: true become: true +# btop fitted to its pane (docs/herdr.md#btop): the launcher takes the place of +# the btop command and runs the installer's btop-bin with a preset of this +# config. btop rewrites its config on exit; every apply puts the saved one back. +- name: Install the btop launcher + ansible.builtin.copy: + src: "{{ factory_btop_launcher_source }}" + dest: "{{ factory_local_bin }}/btop" + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + mode: "0755" + remote_src: true + become: true + +- name: Ensure the btop configuration directory + ansible.builtin.file: + path: "{{ factory_btop_config | dirname }}" + state: directory + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + mode: "0755" + become: true + +- name: Install the btop configuration + ansible.builtin.copy: + src: "{{ code_factory_repo }}/config/btop.conf" + dest: "{{ factory_btop_config }}" + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + mode: "0644" + remote_src: true + become: true + - name: Render the Herdr Spaces reporter unit and timer ansible.builtin.template: src: "{{ item }}.j2" diff --git a/ansible/tasks/tools.yml b/ansible/tasks/tools.yml index 6109895..8eff520 100644 --- a/ansible/tasks/tools.yml +++ b/ansible/tasks/tools.yml @@ -3,7 +3,7 @@ # latest releases tasks/preflight.yml resolved. # # Contract: python3 /scripts/install_tools.py -# --home --tools herdr,node,bun,uv [--npm] [--development] +# --home --tools herdr,node,bun,uv,btop [--npm] [--development] # --resolved # The installer is idempotent and prints a final JSON object # {"changed": bool, "installed": [...]}, which is the only changed signal this diff --git a/ansible/tasks/verify.yml b/ansible/tasks/verify.yml index c21bea2..504908a 100644 --- a/ansible/tasks/verify.yml +++ b/ansible/tasks/verify.yml @@ -37,6 +37,9 @@ factory_user_units ~ '/default.target.wants/herdr.service', factory_herdr_spaces_script, factory_local_bin ~ '/herdr-sidebar-to-client.py', + factory_local_bin ~ '/btop', + factory_local_bin ~ '/btop-bin', + factory_btop_config, factory_user_units ~ '/herdr-spaces.service', factory_user_units ~ '/herdr-spaces.timer', factory_user_units ~ '/timers.target.wants/herdr-spaces.timer', diff --git a/config/btop.conf b/config/btop.conf index 45e5757..e3ac198 100644 --- a/config/btop.conf +++ b/config/btop.conf @@ -23,7 +23,7 @@ disable_presets = "Off" #* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. #* Use whitespace " " as separator between different presets. #* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" -presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" +presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty proc:0:default" #* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. #* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. diff --git a/docs/dependencies.md b/docs/dependencies.md index 65cf501..521be7d 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -13,7 +13,7 @@ Everything the recipe installs, grouped by where it comes from. Nothing is pinne `scripts/install_tools.py`. Native tools are linked into `~/.local/bin`; npm tools are each installed with `npm install` into `~/.local/share/code-factory//` (npm checks the registry integrity) and linked from there. Superseded versions stay on disk. -- Always: herdr ([herdrdev/herdr](https://github.com/herdrdev/herdr/releases/latest)), bun ([oven-sh/bun](https://github.com/oven-sh/bun/releases/latest), the x64 `baseline` build), uv ([astral-sh/uv](https://github.com/astral-sh/uv/releases/latest)), verified against the GitHub release-asset digest. A herdr upgrade rewrites and restarts `herdr.service`. +- Always: herdr ([herdrdev/herdr](https://github.com/herdrdev/herdr/releases/latest)), bun ([oven-sh/bun](https://github.com/oven-sh/bun/releases/latest), the x64 `baseline` build), uv ([astral-sh/uv](https://github.com/astral-sh/uv/releases/latest)), btop ([aristocratos/btop](https://github.com/aristocratos/btop/releases/latest), the static musl build, linked as `btop-bin`; `btop` is the launcher in [btop](herdr.md#btop)), verified against the GitHub release-asset digest. A herdr upgrade rewrites and restarts `herdr.service`. - Always: node, the newest release in the [nodejs.org index](https://nodejs.org/dist/index.json) (not the LTS line), verified against that release's `SHASUMS256.txt`. - `agents` profile, native: gh ([cli/cli](https://github.com/cli/cli/releases/latest)), treehouse ([kunchenguid/treehouse](https://github.com/kunchenguid/treehouse/releases/latest)), verified against the GitHub release-asset digest. - `agents` profile, no-mistakes ([kunchenguid/no-mistakes](https://github.com/kunchenguid/no-mistakes/releases)): the one tool that tracks the prerelease channel. Each apply resolves the newest non-draft release, betas included (not only the latest stable one), and verifies it against the GitHub release-asset digest. @@ -23,7 +23,7 @@ Everything the recipe installs, grouped by where it comes from. Nothing is pinne - `agents` profile, omp plugins: ponytail ([DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)), i-have-adhd ([ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd)) and caveman ([JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman)) from their GitHub marketplaces, installed once and upgraded with `omp plugin upgrade` on every apply. These are the only installs that are not checksum-verified: no publisher posts a checksum for them, so they track each author's default branch and load as agent instructions and hooks. The operator accepted this to keep them at the latest commit. - `development` profile: rustup-init, the version in rustup's [stable release](https://static.rust-lang.org/rustup/release-stable.toml), verified against the `.sha256` published beside it, installing the Rust `stable` toolchain (minimal profile + rustfmt + clippy). Every apply moves the toolchain to the newest stable. -The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); a resolution makes one request per GitHub-hosted tool the host installs, at most eight. Only the tools a run installs are resolved, so a source the host does not use cannot fail it. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply` or `./bootstrap.sh`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. +The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); a resolution makes one request per GitHub-hosted tool the host installs, at most nine. Only the tools a run installs are resolved, so a source the host does not use cannot fail it. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply` or `./bootstrap.sh`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. ## Ubuntu packages diff --git a/docs/herdr.md b/docs/herdr.md index ef3fb10..5b674c9 100644 --- a/docs/herdr.md +++ b/docs/herdr.md @@ -192,6 +192,14 @@ What the mobile layout forces, compared with the desktop sidebar: - A home's counts and shares are on its own agent's entry, so a home with no agent pane carrying a `who` token shows none. Without a `machine` workspace, the machine's shares do not show either. - The issue (`○`) is left out for width. +### btop + +btop draws nothing in a pane smaller than its shown boxes need; it shows `Terminal size too small` instead. All four boxes need 80x24 in btop 1.4 (the cpu box is 60 columns wide, the proc box 44 beside the 36 of mem and net), so a 50-column phone pane is too small. + +`./factory apply` installs the latest btop release as `~/.local/bin/btop-bin`, [`config/btop.conf`](../config/btop.conf) as `~/.config/btop/btop.conf`, and [`maintenance/btop.sh`](../maintenance/btop.sh) as `~/.local/bin/btop`. The launcher starts btop with preset 0 (all four boxes) in a pane of at least 80x24, and otherwise with preset 4 (processes only, which needs 44x16). When a phone attaches to the same session and the pane crosses 80x24, the launcher restarts btop with the other preset. Quitting btop ends the launcher. + +btop saves its config on exit, including the last preset's boxes; the launcher always passes a preset, so that never changes what it shows. Every apply rewrites the config, so make lasting changes in `config/btop.conf`, and keep preset 4 there. + ## Viewing from another machine Sidebar layouts are client-side: Herdr draws the sidebar from the config of the machine you view from, even over `herdr --remote`. The host still reports every token, but the viewing machine needs the host's layout to show them. `./factory apply` installs [`maintenance/herdr-sidebar-to-client.py`](../maintenance/herdr-sidebar-to-client.py) as `~/.local/bin/herdr-sidebar-to-client.py`. On the viewing machine, run it against the host with Python 3 and ssh access, where `` is anything `ssh` accepts, such as `user@host`: diff --git a/maintenance/btop.sh b/maintenance/btop.sh new file mode 100755 index 0000000..d7a7982 --- /dev/null +++ b/maintenance/btop.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# btop that fits its pane, installed as ~/.local/bin/btop (docs/herdr.md#btop). +# All four boxes need 80x24 in btop 1.4 (cpu 60 wide, proc 44 beside mem/net +# 36); a Herdr phone pane is about 50 columns. Smaller panes get preset 4 from +# btop.conf (processes only, 44x16), others preset 0 (all boxes). A resize that +# crosses 80x24 restarts btop with the other preset. +preset() { + set -- $(stty size) + if [ "$1" -ge 24 ] && [ "$2" -ge 80 ]; then echo 0; else echo 4; fi +} +# No job control here, so a background command reads /dev/null unless given +# stdin explicitly. Not /dev/null; do :; done + [ "$again" ] || exit "$rc" +done diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 14e8fae..f7e4e8a 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -63,6 +63,14 @@ "uv-{gnu}-unknown-linux-gnu.tar.gz", {"uv": "uv-*/uv", "uvx": "uv-*/uvx"}, ), + # Linked as btop-bin: ~/.local/bin/btop is the launcher that fits it to the + # pane (maintenance/btop.sh, installed by ansible/tasks/herdr.yml). + "btop": ( + "aristocratos/btop", + "v", + "btop-{gnu}-unknown-linux-musl.tar.gz", + {"btop-bin": "btop/bin/btop"}, + ), # Resolved for the fleet browser ladder (ansible/tasks/fleet-browsers.yml), not installed here. "obscura": ( "h4ckf0r0day/obscura", @@ -515,7 +523,7 @@ def omp(*argv): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--home", type=Path, required=True) - parser.add_argument("--tools", default="herdr,node,bun,uv") + parser.add_argument("--tools", default="herdr,node,bun,uv,btop") parser.add_argument("--npm", action="store_true") parser.add_argument("--development", action="store_true") parser.add_argument( diff --git a/tests/container-smoke.sh b/tests/container-smoke.sh index 7fd33e5..1e3224a 100755 --- a/tests/container-smoke.sh +++ b/tests/container-smoke.sh @@ -228,7 +228,7 @@ herdr_installed_bin() { image_is_current() { local latest versions='map_values(.version? // .)' latest=$(python3 "${CF_ROOT}/scripts/install_tools.py" \ - --home "${HOME}" --tools herdr,node,bun,uv --npm --development --resolve) \ + --home "${HOME}" --tools herdr,node,bun,uv,btop --npm --development --resolve) \ || fail "could not resolve the latest releases" [ "$(jq -cS "${versions}" <<<"${latest}")" = "$(jq -cS "${versions}" "${RESOLVED_STAMP}")" ] \ && return 0 @@ -655,7 +655,7 @@ check_installer_idempotent() { local out out=$(python3 "${CF_ROOT}/scripts/install_tools.py" \ --home "${HOME}" \ - --tools herdr,node,bun,uv \ + --tools herdr,node,bun,uv,btop \ --resolved "$(cat "${RESOLVED_STAMP}")" \ --npm --development 2>&1) || { printf '%s\n' "${out}" | tail -n 20 diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index 1000d5d..6c00fe2 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -105,6 +105,7 @@ def test_download_rejects_plain_http(tmp_path): "kunchenguid/no-mistakes": ("v9.9.10-beta.1", "no-mistakes-v9.9.10-beta.1-linux-amd64.tar.gz"), "kunchenguid/treehouse": ("v9.9.9", "treehouse-v9.9.9-linux-amd64.tar.gz"), "astral-sh/uv": ("9.9.9", "uv-x86_64-unknown-linux-gnu.tar.gz"), + "aristocratos/btop": ("v9.9.9", "btop-x86_64-unknown-linux-musl.tar.gz"), "h4ckf0r0day/obscura": ("v9.9.9", "obscura-x86_64-linux.tar.gz"), "jimididit/koncreet": ("v9.9.9", "koncreet.tar.gz"), } @@ -180,9 +181,19 @@ def release(tag, name, draft=False): def test_latest_releases_are_pinned_to_the_digests_their_publishers_list(monkeypatch): upstream(monkeypatch) latest = installer.resolve_latest("linux-x86_64", EVERYTHING) - for tool in ("herdr", "bun", "gh", "no-mistakes", "treehouse", "uv", "obscura", "koncreet"): + for tool in ( + "herdr", + "bun", + "gh", + "no-mistakes", + "treehouse", + "uv", + "btop", + "obscura", + "koncreet", + ): assert latest[tool]["assets"]["linux-x86_64"]["sha256"] == "a" * 64 - for tool in ("herdr", "bun", "gh", "treehouse", "uv", "obscura", "koncreet"): + for tool in ("herdr", "bun", "gh", "treehouse", "uv", "btop", "obscura", "koncreet"): assert latest[tool]["version"] == "9.9.9" assert latest["gh"]["assets"]["linux-x86_64"]["format"] == "tar" assert latest["bun"]["assets"]["linux-x86_64"]["format"] == "zip" From 4d43abfd1dc8224eef9c33c5d9c02247b67f6d59 Mon Sep 17 00:00:00 2001 From: administrator Date: Thu, 1 Oct 2026 18:20:25 +0000 Subject: [PATCH 3/3] no-mistakes(review): Restart btop with SIGINT so it restores the terminal --- maintenance/btop.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maintenance/btop.sh b/maintenance/btop.sh index d7a7982..a72bf70 100755 --- a/maintenance/btop.sh +++ b/maintenance/btop.sh @@ -16,7 +16,7 @@ while :; do p=$(preset) again= "${0%/*}/btop-bin" -p "$p" "$@" <&3 3<&- & pid=$! - trap '[ "$(preset)" = "$p" ] || { again=1; kill "$pid"; }' WINCH + trap '[ "$(preset)" = "$p" ] || { again=1; kill -INT "$pid"; }' WINCH # wait returns early when WINCH runs the trap; keep waiting until btop exits. while wait "$pid"; rc=$?; kill -0 "$pid" 2>/dev/null; do :; done [ "$again" ] || exit "$rc"