Compare commits

...

10 Commits

Author SHA1 Message Date
yukkop 557b6e9ef0 fix: migrator 2026-07-24 15:21:19 +04:00
yukkop e49f497045 fix: aga 2026-07-24 15:21:11 +04:00
yukkop feb1a48db1 feat: some 2026-07-23 14:19:58 +04:00
yukkop 30732080b7 feat: some 2026-07-21 16:57:10 +04:00
yukkop 80cf1588bb feat: +darwin config for yukkop
runner nix smoke / nix label smoke (push) Has been cancelled
2026-07-19 20:03:18 +04:00
yukkop ef7d1b29f4 feat: +njalla module 2026-07-13 23:37:14 +00:00
yukkop 7e8c6884db feat: +plgo 2026-07-13 13:27:04 +00:00
yukkop 1dd41e608b feat: neuro: stable video diffusion 2026-07-05 16:58:47 +00:00
yukkop e41c3e5a05 ssh fixes 2026-07-04 19:32:08 +00:00
yukkop f473280bf5 fix: matrix hardcode 2026-07-01 09:44:43 +00:00
31 changed files with 3703 additions and 306 deletions
+21
View File
@@ -0,0 +1,21 @@
{
flake,
self,
inputs,
system ? "aarch64-darwin",
...
}: let
name = builtins.baseNameOf ./.;
in inputs.nix-darwin.lib.darwinSystem {
inherit system;
specialArgs = { inherit flake self inputs; };
modules = [
inputs.home-manager.darwinModules.home-manager
{
networking.hostName = name;
nixpkgs.hostPlatform = system;
nixpkgs.overlays = [ self.overlays.default ];
}
./${name}.nix
];
}
+177
View File
@@ -0,0 +1,177 @@
{
flake,
pkgs,
lib,
...
}: let
name = "yukkop";
in {
system.primaryUser = name;
nix.settings.experimental-features = "nix-command flakes";
programs.zsh.enable = true;
services.openssh.enable = true;
users.users.${name} = {
home = "/Users/${name}";
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJf9ljuqny71bZJokebK4Ybfml0MFMCkApS+tbMdBudp u0_a472@localhost"
];
};
environment.systemPackages = with pkgs; [
aerospace
git
moreutils
neovim
tmux
];
launchd.user.agents.aerospace = {
serviceConfig = {
ProgramArguments = [
"${pkgs.aerospace}/Applications/AeroSpace.app/Contents/MacOS/AeroSpace"
];
RunAtLoad = true;
KeepAlive = true;
StandardOutPath = "/tmp/aerospace.out.log";
StandardErrorPath = "/tmp/aerospace.err.log";
};
};
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.backupFileExtension = "backup";
home-manager.sharedModules = [
(flake + "/home/module/program/tmux.nix")
];
home-manager.users.${name} = {
home.stateVersion = "25.11";
home.packages = with pkgs; [
iproute2mac
jujutsu
ripgrep
];
programs.git = {
enable = true;
lfs.enable = true;
settings = {
user.name = name;
user.email = "hectic.yukkop@gmail.com";
push.autoSetupRemote = true;
init.defaultBranch = "master";
};
};
programs.zsh = {
enable = true;
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
history = {
size = 10000;
path = "$HOME/.zsh/.zsh_history";
};
shellAliases = {
drs = "darwin-rebuild switch --flake ~/pj/hearth#'yukkop|aarch64-darwin'";
nv = "nvim";
tmux = "tmux a";
};
initContent = ''
export PATH=/Users/yukkop/.opencode/bin:$PATH
'';
};
xdg.configFile."aerospace/aerospace.toml".text = ''
start-at-login = false
enable-normalization-flatten-containers = true
enable-normalization-opposite-orientation-for-nested-containers = true
default-root-container-layout = 'tiles'
default-root-container-orientation = 'auto'
accordion-padding = 30
on-focused-monitor-changed = ['move-mouse monitor-lazy-center']
automatically-unhide-macos-hidden-apps = false
[exec]
inherit-env-vars = true
[exec.env-vars]
PATH = '/run/current-system/sw/bin:/etc/profiles/per-user/yukkop/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:''${PATH}'
[gaps]
inner.horizontal = 8
inner.vertical = 8
outer.left = 8
outer.bottom = 8
outer.top = 8
outer.right = 8
[mode.main.binding]
alt-enter = 'exec-and-forget open -n /System/Applications/Utilities/Terminal.app'
alt-slash = 'layout tiles horizontal vertical'
alt-comma = 'layout accordion horizontal vertical'
alt-f = 'fullscreen'
alt-h = 'focus left'
alt-j = 'focus down'
alt-k = 'focus up'
alt-l = 'focus right'
alt-shift-h = 'move left'
alt-shift-j = 'move down'
alt-shift-k = 'move up'
alt-shift-l = 'move right'
alt-minus = 'resize smart -50'
alt-equal = 'resize smart +50'
alt-1 = 'workspace 1'
alt-2 = 'workspace 2'
alt-3 = 'workspace 3'
alt-4 = 'workspace 4'
alt-5 = 'workspace 5'
alt-6 = 'workspace 6'
alt-7 = 'workspace 7'
alt-8 = 'workspace 8'
alt-9 = 'workspace 9'
alt-shift-1 = 'move-node-to-workspace 1'
alt-shift-2 = 'move-node-to-workspace 2'
alt-shift-3 = 'move-node-to-workspace 3'
alt-shift-4 = 'move-node-to-workspace 4'
alt-shift-5 = 'move-node-to-workspace 5'
alt-shift-6 = 'move-node-to-workspace 6'
alt-shift-7 = 'move-node-to-workspace 7'
alt-shift-8 = 'move-node-to-workspace 8'
alt-shift-9 = 'move-node-to-workspace 9'
alt-tab = 'workspace-back-and-forth'
alt-shift-tab = 'move-workspace-to-monitor --wrap-around next'
alt-shift-semicolon = 'mode service'
[mode.service.binding]
esc = ['reload-config', 'mode main']
r = ['flatten-workspace-tree', 'mode main']
f = ['layout floating tiling', 'mode main']
b = ['balance-sizes', 'mode main']
backspace = ['close-all-windows-but-current', 'mode main']
alt-shift-h = ['join-with left', 'mode main']
alt-shift-j = ['join-with down', 'mode main']
alt-shift-k = ['join-with up', 'mode main']
alt-shift-l = ['join-with right', 'mode main']
'';
};
system.stateVersion = 6;
}
Generated
+22
View File
@@ -688,6 +688,27 @@
"url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1772129556,
"narHash": "sha256-Utk0zd8STPsUJPyjabhzPc5BpPodLTXrwkpXBHYnpeg=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "ebec37af18215214173c98cf6356d0aca24a2585",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "nix-darwin-25.11",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-minecraft": {
"inputs": {
"flake-compat": "flake-compat_3",
@@ -967,6 +988,7 @@
"hyprland": "hyprland",
"impermanence": "impermanence",
"mechabellum-replay-analysis": "mechabellum-replay-analysis",
"nix-darwin": "nix-darwin",
"nix-minecraft": "nix-minecraft",
"nixos-anywhere": "nixos-anywhere",
"nixos-hardware": "nixos-hardware",
+7
View File
@@ -42,6 +42,10 @@
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-darwin = {
url = "github:nix-darwin/nix-darwin/nix-darwin-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-wsl = {
url = "github:nix-community/NixOS-WSL";
inputs.nixpkgs.follows = "nixpkgs";
@@ -123,5 +127,8 @@
"tenix|x86_64-linux" = import ./nixos/system/tenix { inherit flake self inputs; system = "x86_64-linux"; };
"hectic-lab|x86_64-linux" = import ./nixos/system/hectic-lab { inherit flake self inputs; system = "x86_64-linux"; };
};
darwinConfigurations = {
"yukkop|aarch64-darwin" = import ./darwin/system/yukkop { inherit flake self inputs; system = "aarch64-darwin"; };
};
};
}
+30
View File
@@ -0,0 +1,30 @@
{ pkgs, ... }: {
programs.tmux = {
enable = true;
plugins = with pkgs.tmuxPlugins; [ resurrect continuum ];
keyMode = "vi";
escapeTime = 500;
historyLimit = 50000;
newSession = true;
extraConfig = ''
# resurrect
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
resurrect_dir="$HOME/.tmux/resurrect"
set -g @resurrect-dir $resurrect_dir
set -g @resurrect-hook-post-save-all 'target=$(readlink -f $resurrect_dir/last); sed "s| --cmd .*-vim-pack-dir||g; s|/etc/profiles/per-user/$USER/bin/||g; s|/home/$USER/.nix-profile/bin/||g" $target | sponge $target'
# continuum
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key O select-pane -t :.-
'';
};
}
+28 -1
View File
@@ -26,12 +26,39 @@
"aarch64-darwin"
];
cudaUnfreeNames = [
"cuda_nvcc"
"cuda_cudart"
"cuda_cuobjdump"
"cuda_cupti"
"cuda_nvdisasm"
"cuda_cccl"
"cuda_nvml_dev"
"cuda_nvrtc"
"cuda_nvtx"
"cuda_profiler_api"
"libcusparse_lt"
"libcublas"
"libcufft"
"libcufile"
"libcurand"
"libcusolver"
"libnvjitlink"
"libcusparse"
"cudnn"
];
cudaUnfreePredicate = pkg:
builtins.elem (nixpkgs.lib.getName pkg) cudaUnfreeNames;
forSystemsWithPkgs = supportedSystems: pkgOverlays: f:
builtins.foldl' (
acc: system: let
pkgs = import nixpkgs {
inherit system;
overlays = pkgOverlays;
config.allowUnfreePredicate = cudaUnfreePredicate;
};
systemOutputs = f {
system = system;
@@ -58,7 +85,7 @@
else {};
in {
# -- For all systems --
inherit dotEnv minorEnvironment parseEnv forAllSystemsWithPkgs forSystemsWithPkgs commonSystems AllSystems;
inherit dotEnv minorEnvironment parseEnv forAllSystemsWithPkgs forSystemsWithPkgs commonSystems AllSystems cudaUnfreeNames cudaUnfreePredicate;
forSystems = systems: nixpkgs.lib.genAttrs systems;
forAllSystems = nixpkgs.lib.genAttrs AllSystems;
+134
View File
@@ -0,0 +1,134 @@
{ ... }:
{
lib,
config,
...
}: let
cfg = config.hectic.hardware.njalla;
in {
options.hectic.hardware.njalla = {
enable = lib.mkEnableOption "Enable njalla hardware configurations";
ipv4 = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
example = "185.193.126.103";
description = ''
Njalla IPv4 address assigned to the host.
'';
};
ipv4PrefixLength = lib.mkOption {
type = lib.types.int;
default = 24;
example = 24;
description = ''
Njalla IPv4 prefix length.
'';
};
ipv4Gateway = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
default = "185.193.126.1";
example = "185.193.126.1";
description = ''
Njalla IPv4 gateway.
'';
};
ipv6 = lib.mkOption {
type = lib.types.strMatching "^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
example = "2a0a:3840:1337:126:0:b9c1:7e67:1337";
description = ''
Njalla IPv6 address assigned to the host.
'';
};
ipv6PrefixLength = lib.mkOption {
type = lib.types.int;
default = 64;
example = 64;
description = ''
Njalla IPv6 prefix length.
'';
};
ipv6Gateway = lib.mkOption {
type = lib.types.strMatching "^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
default = "2a0a:3840:1337:126::1";
example = "2a0a:3840:1337:126::1";
description = ''
Njalla IPv6 gateway.
'';
};
networkMatchConfigName = lib.mkOption {
type = lib.types.str;
default = "eth0";
example = "eth0";
description = ''
Njalla container network interface name.
'';
};
device = lib.mkOption {
type = lib.types.str;
default = "/dev/vda";
example = "/dev/disk/by-id/virtio-root";
description = ''
Njalla installation disk for disko/nixos-anywhere.
`/dev/vda` is the default block device visible on the inspected Njalla
host. Prefer a stable `/dev/disk/by-id/...` path when available.
'';
};
enableDisko = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to provide a disko layout for nixos-anywhere installs.
'';
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
boot.isContainer = true;
networking.useDHCP = false;
networking.useNetworkd = true;
systemd.network.enable = true;
systemd.network.networks."30-wan" = {
matchConfig.Name = cfg.networkMatchConfigName;
networkConfig.DHCP = "no";
address = [
"${cfg.ipv4}/${toString cfg.ipv4PrefixLength}"
"${cfg.ipv6}/${toString cfg.ipv6PrefixLength}"
];
routes = [
{ Gateway = cfg.ipv4Gateway; }
{ Gateway = cfg.ipv6Gateway; }
];
};
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
}
(lib.mkIf cfg.enableDisko {
boot.loader.grub.device = cfg.device;
disko.devices.disk.main = {
type = "disk";
device = cfg.device;
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02";
priority = 1;
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
})
]);
}
+1 -30
View File
@@ -25,36 +25,7 @@ in {
programs.bash.shellAliases.tmux = "tmux a";
home-manager.sharedModules = [
{
programs.tmux = {
enable = true;
plugins = with pkgs.tmuxPlugins; [ resurrect continuum ];
keyMode = "vi";
escapeTime = 500;
historyLimit = 50000;
newSession = true;
extraConfig = ''
# resurrect
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
resurrect_dir="$HOME/.tmux/resurrect"
set -g @resurrect-dir $resurrect_dir
set -g @resurrect-hook-post-save-all 'target=$(readlink -f $resurrect_dir/last); sed "s| --cmd .*-vim-pack-dir||g; s|/etc/profiles/per-user/$USER/bin/||g; s|/home/$USER/.nix-profile/bin/||g" $target | sponge $target'
# continuum
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key O select-pane -t :.-
'';
};
}
(flake + "/home/module/program/tmux.nix")
];
home-manager.users.root.home.stateVersion = lib.mkDefault "25.05";
@@ -0,0 +1,153 @@
{ ... }: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.services.stable-video-diffusion;
in {
options.hectic.services.stable-video-diffusion = {
enable = lib.mkEnableOption "local Stable Video Diffusion HTTP API";
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address the Stable Video Diffusion API binds to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 7861;
description = "Port the Stable Video Diffusion API binds to.";
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.hectic.stable-video-diffusion-api;
defaultText = lib.literalExpression "pkgs.hectic.stable-video-diffusion-api";
description = "Package providing the Stable Video Diffusion API executable.";
};
modelId = lib.mkOption {
type = lib.types.str;
default = "stabilityai/stable-video-diffusion-img2vid-xt";
description = "Model identifier loaded by the Stable Video Diffusion API.";
};
device = lib.mkOption {
type = with lib.types; nullOr (enum [ "cpu" "cuda" ]);
default = null;
description = ''
Torch device requested from the Stable Video Diffusion API. When null,
the package keeps its own auto-detection behavior.
'';
};
libraryPath = lib.mkOption {
type = with lib.types; listOf str;
default = [];
description = ''
Runtime library paths added to LD_LIBRARY_PATH. CUDA/NVIDIA deployments
should include /run/opengl-driver/lib so libcuda.so is visible to torch.
'';
};
stateDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/stable-video-diffusion-api";
description = "Persistent state directory for the Stable Video Diffusion API.";
};
cacheDir = lib.mkOption {
type = lib.types.str;
default = "/var/cache/stable-video-diffusion-api";
description = "Cache directory for downloaded model and runtime artifacts.";
};
outputDir = lib.mkOption {
type = lib.types.str;
default = "${cfg.stateDir}/outputs";
defaultText = lib.literalExpression ''"\${config.hectic.services.stable-video-diffusion.stateDir}/outputs"'';
description = "Directory where generated video outputs are written.";
};
environmentFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = ''
Optional environment file for secrets or runtime overrides. Values from
the unit environment define SVD_API_HOST, SVD_API_PORT, SVD_MODEL_ID,
SVD_STATE_DIR, SVD_CACHE_DIR, SVD_OUTPUT_DIR, and optionally SVD_DEVICE
and LD_LIBRARY_PATH by default.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the API port in the firewall.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.device != "cuda" || cfg.libraryPath != [];
message = "hectic.services.stable-video-diffusion.libraryPath must include the NVIDIA runtime library path when device is cuda.";
}
];
users.users.stable-video-diffusion = {
isSystemUser = true;
group = "stable-video-diffusion";
};
users.groups.stable-video-diffusion = {};
systemd.tmpfiles.rules = [
"d ${cfg.stateDir} 0750 stable-video-diffusion stable-video-diffusion -"
"d ${cfg.cacheDir} 0750 stable-video-diffusion stable-video-diffusion -"
"d ${cfg.outputDir} 0750 stable-video-diffusion stable-video-diffusion -"
];
systemd.services.stable-video-diffusion-api = {
description = "Stable Video Diffusion HTTP API";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
SVD_API_HOST = cfg.host;
SVD_API_PORT = toString cfg.port;
SVD_MODEL_ID = cfg.modelId;
SVD_STATE_DIR = cfg.stateDir;
SVD_CACHE_DIR = cfg.cacheDir;
SVD_OUTPUT_DIR = cfg.outputDir;
}
// lib.optionalAttrs (cfg.device != null) {
SVD_DEVICE = cfg.device;
}
// lib.optionalAttrs (cfg.libraryPath != []) {
LD_LIBRARY_PATH = lib.concatStringsSep ":" cfg.libraryPath;
};
serviceConfig = lib.mkMerge [
{
Type = "simple";
User = "stable-video-diffusion";
Group = "stable-video-diffusion";
WorkingDirectory = cfg.stateDir;
ExecStart = lib.getExe' cfg.package "stable-video-diffusion-api";
Restart = "on-failure";
RestartSec = "5s";
TimeoutStopSec = "30s";
KillSignal = "SIGTERM";
KillMode = "mixed";
StandardOutput = "journal";
StandardError = "journal";
}
(lib.mkIf (cfg.environmentFile != null) {
EnvironmentFile = cfg.environmentFile;
})
];
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
};
}
@@ -1,230 +0,0 @@
# Matrix Cluster Failover Runbook (`accord.tube`)
Primary: `hectic-lab` (NL, `128.140.75.58`)
Standby: `bfs.poland.xray` (PL, `91.198.166.181`)
Module: `hectic.generic.matrix-cluster` (`nixos/module/generic/matrix-cluster.nix`).
Shared secrets: `sus/matrix-cluster.yaml`.
All `psql` and `pg_ctl` invocations use PostgreSQL **17** at data dir
`/var/lib/postgresql/17`.
## Initial setup
### 1. Provision shared SOPS file (`sus/matrix-cluster.yaml`)
On a workstation with both yukkop and yukkop-alt age keys available:
```sh
sudo cat /var/lib/matrix-synapse/homeserver.signing.key # on NL (hectic-lab)
# Copy the single line value into the buffer for the next step.
sops sus/matrix-cluster.yaml
```
Populate the editor with:
```yaml
matrix:
signing-key: <paste verbatim signing-key line from NL>
postgres-replication-password: <openssl rand -base64 32>
object-storage:
credentials: |
ACCESS_KEY_ID=<verbatim copy from sus/hectic-lab.yaml>
SECRET_ACCESS_KEY=<verbatim copy from sus/hectic-lab.yaml>
porkbun-api-key: <PORKBUN_API_KEY>
porkbun-secret-api-key: <PORKBUN_SECRET_API_KEY>
```
Verify recipients:
```sh
sops updatekeys sus/matrix-cluster.yaml
sops -d sus/matrix-cluster.yaml | grep -E 'signing-key|porkbun-api-key|object-storage'
```
Expected: all five keys present, exit 0.
### 2. Deploy NL primary first
```sh
nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux' --target-host root@128.140.75.58
```
Verify on NL:
```sh
sudo systemctl status matrix-synapse postgresql matrix-cluster-replication-password
sudo -u postgres psql -c "select rolname, rolreplication from pg_roles where rolname='replication';"
# Expected: replication | t
```
### 3. Seed PL replica with `pg_basebackup`
On PL:
```sh
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17
sudo -u postgres install -d -m 0700 /var/lib/postgresql/17
sudo -u postgres PGPASSWORD="$(sudo cat /run/secrets/matrix/postgres-replication-password)" \
pg_basebackup \
-h 128.140.75.58 \
-p 5432 \
-U replication \
-D /var/lib/postgresql/17 \
-Fp -Xs -P -R \
--no-password
```
`-R` writes `standby.signal` and an initial `primary_conninfo`. The
matrix-cluster module's `matrix-cluster-standby-bootstrap` service will
overwrite `primary_conninfo` to use a libpq passfile on next boot.
### 4. Deploy PL standby
```sh
nixos-rebuild switch --flake .#'bfs.poland.xray|x86_64-linux' --target-host root@91.198.166.181
sudo systemctl start postgresql
```
Verify streaming on NL:
```sh
sudo -u postgres psql -c 'select client_addr, state, sync_state from pg_stat_replication;'
# Expected: 91.198.166.181 | streaming | async
```
Verify standby on PL:
```sh
sudo -u postgres psql -c 'select pg_is_in_recovery();'
# Expected: t
sudo systemctl is-active matrix-synapse
# Expected: inactive (standby keeps Synapse off)
```
### 5. Remove duplicate S3 credentials from `sus/hectic-lab.yaml`
Only AFTER NL is confirmed healthy reading from the new shared file:
```sh
sops sus/hectic-lab.yaml
# Delete the matrix/object-storage/credentials block.
sudo nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux'
```
## Normal operations
```sh
# NL: replication health
sudo -u postgres psql -c 'select * from pg_stat_replication;'
# Expected: 1 row, state=streaming, sync_state=async
# PL: replay status
sudo -u postgres psql -c 'select now() - pg_last_xact_replay_timestamp() as lag;'
# Both: cert renewal
sudo systemctl status acme-accord.tube.timer
sudo journalctl -u acme-accord.tube.service --since '24 hours ago'
# Synapse health (NL primary)
curl -sf https://accord.tube/_matrix/client/versions | head
```
## Planned failover (NL -> PL)
```sh
# 1. Drain NL: stop accepting writes.
sudo systemctl stop matrix-synapse
sudo systemctl stop postgresql # ensure no new WAL after this point
# 2. Promote PL replica.
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote
# Wait until pg_is_in_recovery() returns f:
sudo -u postgres psql -c 'select pg_is_in_recovery();'
# 3. Make the role switch declarative before rebuilding.
# Edit the flake so rebuilds match the promoted database state:
# - nixos/system/bfs.poland.xray/bfs.poland.xray.nix:
# hectic.generic.matrix-cluster.role = "primary";
# hectic.generic.matrix-cluster.overrideEnableSynapse = true;
# hectic.generic.matrix-cluster.secretsFile = config.sops.secrets."matrix/secrets".path;
# - nixos/system/hectic-lab/hectic-lab.nix:
# hectic.generic.matrix-cluster.role = "standby";
# hectic.generic.matrix-cluster.overrideEnableSynapse = false;
# hectic.generic.matrix-cluster.replication.peerHost = "91.198.166.181";
# hectic.generic.matrix-cluster.replication.allowedSourceIPs = [ "128.140.75.58/32" ];
# (You will also need a matrix/secrets entry on PL - copy from NL via SOPS.)
sudo nixos-rebuild switch --flake .#'bfs.poland.xray|x86_64-linux'
sudo nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux'
sudo systemctl status matrix-synapse
# 4. Swap DNS A record at Porkbun:
# accord.tube A 91.198.166.181 (was 128.140.75.58)
# TTL: set to 300 in advance of any planned failover.
# Porkbun UI: https://porkbun.com/account/domainsSpeedy -> accord.tube -> DNS -> edit A record.
# Or via API:
sudo curl -sX POST https://api.porkbun.com/api/json/v3/dns/editByNameType/accord.tube/A \
-H 'content-type: application/json' \
-d "$(jq -n --arg k "$PORKBUN_API_KEY" --arg s "$PORKBUN_SECRET_API_KEY" \
'{secretapikey:$s,apikey:$k,content:"91.198.166.181",ttl:"300"}')"
# 5. Federation smoke test.
curl -s 'https://federationtester.matrix.org/api/report?server_name=accord.tube' | jq .FederationOK
# Expected: true
```
Expected after the rebuilds:
- `bfs.poland.xray` evaluates and runs as `role = "primary"`.
- `hectic-lab` evaluates as `role = "standby"` with Synapse forced off.
- Future `nixos-rebuild` runs preserve the promoted topology instead of reapplying standby settings to PL.
## Failback (PL -> NL)
```sh
# 1. Stop NL postgres if still up; clear its data dir.
sudo systemctl stop postgresql matrix-synapse
sudo rm -rf /var/lib/postgresql/17
# 2. Re-seed NL from PL (now the live primary).
sudo -u postgres install -d -m 0700 /var/lib/postgresql/17
sudo -u postgres PGPASSWORD="$(sudo cat /run/secrets/matrix/postgres-replication-password)" \
pg_basebackup -h 91.198.166.181 -p 5432 -U replication \
-D /var/lib/postgresql/17 -Fp -Xs -P -R --no-password
# 3. Temporarily flip roles in the flake:
# - hectic-lab.nix: role = "standby"; peerHost = "91.198.166.181";
# - bfs.poland.xray.nix: role = "primary"; peerHost = "128.140.75.58";
# Rebuild both.
# 4. Once NL is streaming green, do the reverse failover dance:
sudo systemctl stop matrix-synapse # on PL
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote # on NL
# Then revert the flake role assignments back to NL=primary / PL=standby and
# rebuild both hosts.
# 5. Swap DNS back at Porkbun (A -> 128.140.75.58).
```
## Disaster recovery (NL permanently lost)
```sh
# 1. Promote PL as the new permanent primary.
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote
# 2. Edit nixos/system/bfs.poland.xray/bfs.poland.xray.nix:
# hectic.generic.matrix-cluster.role = "primary";
# hectic.generic.matrix-cluster.overrideEnableSynapse = lib.mkForce null;
# hectic.generic.matrix-cluster.replication.peerHost = "<new-standby-ip>";
# hectic.generic.matrix-cluster.replication.allowedSourceIPs = [ "<new-standby-ip>/32" ];
# 3. Provision a new host (any region with Porkbun-managed DNS) and import
# self.nixosModules.matrix-cluster with role = "standby" pointed at PL's IP.
# 4. Bootstrap the new standby via pg_basebackup from PL exactly as in
# "Initial setup" step 3, replacing 128.140.75.58 with PL's IP.
# 5. Update Porkbun A record to PL's IP permanently.
```
@@ -9,17 +9,11 @@
config,
...
}: let
matrixBackend = "https://128.140.75.58";
matrixHost = "accord.tube";
jitsiHost = "meet.accord.tube";
elementEntryDomain = "element.bfs.band";
polandEntryDomain = "bfs.band";
matrixClusterSopsFile = flake + "/sus/matrix-cluster.yaml";
backendProxyConfig = ''
proxy_ssl_server_name on;
proxy_ssl_name ${matrixHost};
proxy_set_header Host ${matrixHost};
'';
in {
imports = [
self.nixosModules.xray-system
@@ -47,7 +41,6 @@ in {
credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
};
replication = {
peerHost = "128.140.75.58";
passwordFile = config.sops.secrets."matrix/postgres-replication-password".path;
};
acme = {
@@ -133,19 +126,17 @@ in {
};
locations."= /livekit/jwt" = {
proxyPass = "${matrixBackend}/livekit/jwt";
extraConfig = backendProxyConfig;
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/";
};
locations."^~ /livekit/jwt/" = {
proxyPass = "${matrixBackend}/livekit/jwt/";
extraConfig = backendProxyConfig;
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/";
};
locations."= /livekit/sfu" = {
proxyPass = "${matrixBackend}/livekit/sfu";
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/";
proxyWebsockets = true;
extraConfig = backendProxyConfig + ''
extraConfig = ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
@@ -156,9 +147,9 @@ in {
};
locations."^~ /livekit/sfu/" = {
proxyPass = "${matrixBackend}/livekit/sfu/";
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/";
proxyWebsockets = true;
extraConfig = backendProxyConfig + ''
extraConfig = ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
@@ -169,13 +160,14 @@ in {
};
locations."^~ /_matrix/" = {
proxyPass = "${matrixBackend}/_matrix/";
extraConfig = backendProxyConfig;
proxyPass = "http://127.0.0.1:8008";
extraConfig = ''
client_max_body_size ${config.hectic.generic.matrix-cluster.maxUploadSize};
'';
};
locations."^~ /_synapse/client/" = {
proxyPass = "${matrixBackend}/_synapse/client/";
extraConfig = backendProxyConfig;
proxyPass = "http://127.0.0.1:8008";
};
};
@@ -0,0 +1,74 @@
{ pkgs, ... }:
let
port = 22222;
stateDir = "/var/lib/experimental-sshd";
configFile = pkgs.writeText "experimental-sshd_config" ''
Port 22222
ListenAddress 0.0.0.0
ListenAddress ::
HostKey ${stateDir}/ssh_host_ed25519_key
HostKey ${stateDir}/ssh_host_rsa_key
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password
UsePAM yes
AuthenticationMethods publickey
AuthorizedKeysFile %h/.ssh/authorized_keys /etc/ssh/authorized_keys.d/%u
LogLevel DEBUG3
VersionAddendum none
HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-ed25519
KexAlgorithms curve25519-sha256,diffie-hellman-group14-sha256
Ciphers aes256-ctr,aes128-ctr
MACs hmac-sha2-512,hmac-sha2-256
'';
keygenScript = pkgs.writeShellScript "experimental-sshd-keygen" ''
set -eu
${pkgs.coreutils}/bin/mkdir -p -m 0700 ${stateDir}
if [ ! -f ${stateDir}/ssh_host_ed25519_key ]; then
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f ${stateDir}/ssh_host_ed25519_key -N ""
fi
if [ ! -f ${stateDir}/ssh_host_rsa_key ]; then
${pkgs.openssh}/bin/ssh-keygen -t rsa -b 4096 -f ${stateDir}/ssh_host_rsa_key -N ""
fi
'';
in
{
environment.etc."ssh/experimental-sshd_config".source = configFile;
networking.firewall.allowedTCPPorts = [ port ];
systemd.services.experimental-sshd-keygen = {
description = "Generate experimental SSH host keys";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StateDirectory = "experimental-sshd";
ExecStart = keygenScript;
};
};
systemd.services.experimental-sshd = {
description = "Experimental SSH daemon";
wantedBy = [ "multi-user.target" ];
wants = [ "experimental-sshd-keygen.service" ];
after = [ "network.target" "experimental-sshd-keygen.service" ];
serviceConfig = {
Type = "simple";
StateDirectory = "experimental-sshd";
RuntimeDirectory = "experimental-sshd";
ExecStart = "${pkgs.openssh}/bin/sshd -D -e -f /etc/ssh/experimental-sshd_config";
};
preStart = ''
${pkgs.openssh}/bin/sshd -t -f /etc/ssh/experimental-sshd_config
'';
};
}
+1
View File
@@ -57,6 +57,7 @@ in {
(import ./attic.nix { inherit flake self inputs domain; })
(import ./containers.nix { inherit flake self inputs; })
./experimental-sshd.nix
(import ./ente.nix { inherit domain; })
(import ./mechabellum.nix { inherit flake self inputs domain; })
(import (./. + "/sentinèlla.nix") { inherit flake self inputs domain; })
+2 -22
View File
@@ -15,32 +15,12 @@ in self.lib.nixpkgs-lib.nixosSystem {
self.overlays.default
inputs.nix-minecraft.overlay
];
config.allowUnfreePredicate = pkg: builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
config.allowUnfreePredicate = pkg:
self.lib.cudaUnfreePredicate pkg || builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
"minecraft-server"
"neoforge"
"nvidia-x11"
"cuda_nvcc"
"cuda_cudart"
"cuda_cuobjdump"
"cuda_cupti"
"cuda_nvdisasm"
"cuda_cccl"
"cuda_nvml_dev"
"cuda_nvrtc"
"cuda_nvtx"
"cuda_profiler_api"
"libcusparse_lt"
"libcublas"
"libcufft"
"libcufile"
"libcurand"
"libcusolver"
"libnvjitlink"
"libcusparse"
"cudnn"
];
# jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x)
config.permittedInsecurePackages = [
+18
View File
@@ -17,6 +17,11 @@
ollamaServiceBundledLibraryPath = "${ollamaPrebuilt}/lib/ollama:${ollamaPrebuilt}/lib/ollama/cuda_v12:${ollamaPrebuilt}/lib/ollama/cuda_v13";
stableVideoDiffusionLibraryPath = lib.makeLibraryPath [
pkgs.stdenv.cc.cc.lib
pkgs.zlib
];
ollamaPrebuilt = pkgs.stdenvNoCC.mkDerivation {
pname = "ollama";
version = "0.22.1";
@@ -176,6 +181,19 @@ in {
openFirewall = false;
};
hectic.services.stable-video-diffusion = {
enable = true;
host = "127.0.0.1";
port = 7861;
package = pkgs.hectic.stable-video-diffusion-api;
device = "cuda";
libraryPath = [
stableVideoDiffusionLibraryPath
"/run/opengl-driver/lib"
];
openFirewall = false;
};
networking = {
networkmanager.enable = true;
useDHCP = lib.mkDefault true;
+3
View File
@@ -132,6 +132,7 @@ in {
github-gh-tl = pkgs.callPackage ./github/gh-tl.nix {};
supabase-with-env-collection = pkgs.callPackage ./supabase-with-env-collection.nix {};
migration-name = pkgs.callPackage ./migration-name.nix {};
plgo = pkgs.callPackage ./plgo {};
pg-from = pkgs.callPackage ./postgres/pg-from/default.nix rust.commonArgs;
pg-schema = pkgs.callPackage ./postgres/pg-schema/default.nix rust.commonArgs;
pg_wdumpall = pkgs.callPackage ./postgres/pg_wdumpall.nix rust.commonArgs;
@@ -148,6 +149,7 @@ in {
deploy = pkgs.callPackage ./deploy { inherit inputs; };
element-web = pkgs.callPackage ./element-web {};
shellplot = pkgs.callPackage ./shellplot {};
which-country-rs = pkgs.callPackage ./which-country-rs {};
onlinepubs2man = pkgs.callPackage ./onlinepubs2man {};
migrator = pkgs.callPackage ./migrator { inherit self; };
"parse-uri" = pkgs.callPackage ./parse-uri {};
@@ -174,5 +176,6 @@ in {
pg-15-ext-smtp-client = buildSmtpExt pkgs "15";
pg-15-ext-plhaskell = buildPlHaskellExt pkgs "15";
pg-15-ext-plsh = buildPlShExt pkgs "15";
stable-video-diffusion-api = pkgs.callPackage ./stable-video-diffusion-api {};
media-browser = pkgs.callPackage ./media-browser {};
}
+21 -4
View File
@@ -434,6 +434,8 @@ migrate_down() {
target_migration=$(printf '%s' "$fs_migrations" | sed -n "${target_line}p")
printf '%s' "$target_migration"
fi
return 0
}
migrate_up() {
@@ -488,6 +490,7 @@ migrate_up() {
fi
printf '%s' "$target_migration"
return 0
}
migrate_to() {
@@ -518,6 +521,8 @@ migrate_to() {
printf '%s' "$migration_name"
;;
esac
return 0
}
migration_list() {
@@ -566,7 +571,10 @@ index_of() {
# no subshell, no pipeline
while IFS= read -r m; do
[ "$m" = "$name" ] && { printf '%s\n' "$i"; return 0; }
if [ "$m" = "$name" ]; then
printf '%s\n' "$i"
return 0
fi
i=$((i+1))
done <<EOF
$list
@@ -697,14 +705,20 @@ migrate() {
log info "Migration history validation: ${GREEN}OK${NC} (${WHITE}$i${NC} migrations match)"
eval "set -- $MIGRATOR_REMAINING_ARS"
target_migration="$("migrate_$MIGRATE_SUBCOMMAND" "$@")"
if ! target_migration="$("migrate_$MIGRATE_SUBCOMMAND" "$@")"; then
log error "failed to resolve migration target for ${WHITE}migrate $MIGRATE_SUBCOMMAND${NC}"
exit 1
fi
if [ -z "$db_migrations" ]; then
log info "starting from clean migration state"
current_idx=0
else
current_migration=$(printf '%s\n' "$db_migrations" | tail -n1)
current_idx=$(index_of "$fs_migrations" "$current_migration")
if ! current_idx=$(index_of "$fs_migrations" "$current_migration"); then
log error "current database migration is missing from filesystem: ${WHITE}$current_migration${NC}"
exit 2
fi
fi
log debug "filesystem migrations: ${WHITE}$fs_migrations${NC}"
@@ -713,7 +727,10 @@ migrate() {
if [ -z "$target_migration" ]; then
target_idx=0
else
target_idx=$(index_of "$fs_migrations" "$target_migration")
if ! target_idx=$(index_of "$fs_migrations" "$target_migration"); then
log error "target migration is missing from filesystem: ${WHITE}$target_migration${NC}"
exit 2
fi
fi
log debug "migration indexes: current=${WHITE}$current_idx${NC} target=${WHITE}$target_idx${NC}"
+31
View File
@@ -0,0 +1,31 @@
{
buildGoModule,
fetchFromGitHub,
lib,
}:
buildGoModule {
pname = "plgo";
version = "0.2.1-unstable-2026-04-04";
src = fetchFromGitHub {
owner = "mgr43";
repo = "plgo";
rev = "492abd81f50f5f588794bd3f9fa103d14546b275";
hash = "sha256-Ifd2IFA62VtkqCUP0/cD3jT4SqkjqwCpHVolslQbwkA=";
};
vendorHash = "sha256-KIVHepJRfVa0bkTF1A4g6ZFGxDy8OPzY3iwV3MhRyVA=";
proxyVendor = true;
subPackages = [ "cmd/plgo" ];
doCheck = false;
meta = {
description = "Write PostgreSQL extensions in Go";
homepage = "https://github.com/mgr43/plgo";
license = lib.licenses.bsd3;
mainProgram = "plgo";
};
}
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""Local HTTP API for Diffusers Stable Video Diffusion image-to-video."""
import os
import threading
import uuid
from importlib import import_module
from pathlib import Path
from typing import Any
torch = import_module("torch")
uvicorn = import_module("uvicorn")
diffusers = import_module("diffusers")
diffusers_utils = import_module("diffusers.utils")
fastapi = import_module("fastapi")
pil_image = import_module("PIL.Image")
pil_unidentified_image_error = import_module("PIL").UnidentifiedImageError
FastAPI = fastapi.FastAPI
File = fastapi.File
Form = fastapi.Form
HTTPException = fastapi.HTTPException
Request = fastapi.Request
UploadFile = fastapi.UploadFile
StableVideoDiffusionPipeline = diffusers.StableVideoDiffusionPipeline
export_to_video = diffusers_utils.export_to_video
HOST = os.environ.get("SVD_API_HOST", "127.0.0.1")
PORT = int(os.environ.get("SVD_API_PORT", "8000"))
MODEL_ID = os.environ.get("SVD_MODEL_ID", "stabilityai/stable-video-diffusion-img2vid-xt")
CACHE_DIR = os.environ.get("SVD_CACHE_DIR") or os.environ.get("HF_HOME")
OUTPUT_DIR = Path(os.environ.get("SVD_OUTPUT_DIR", "/var/lib/stable-video-diffusion-api/outputs"))
DEVICE = os.environ.get("SVD_DEVICE", "cuda" if torch.cuda.is_available() else "cpu")
DTYPE = os.environ.get("SVD_DTYPE", "float16")
ENABLE_CPU_OFFLOAD = os.environ.get("SVD_ENABLE_CPU_OFFLOAD", "0").lower() in ("1", "true", "yes", "on")
DEFAULT_DECODE_CHUNK_SIZE = int(os.environ.get("SVD_DECODE_CHUNK_SIZE", "8"))
DEFAULT_FPS = int(os.environ.get("SVD_FPS", "7"))
app = FastAPI(title="Stable Video Diffusion API")
pipeline_lock = threading.Lock()
pipeline: Any | None = None
def torch_dtype() -> Any:
dtypes = {
"float16": torch.float16,
"fp16": torch.float16,
"float32": torch.float32,
"fp32": torch.float32,
"bfloat16": torch.bfloat16,
"bf16": torch.bfloat16,
}
try:
return dtypes[DTYPE.lower()]
except KeyError as exc:
raise RuntimeError(f"Unsupported SVD_DTYPE: {DTYPE}") from exc
def get_pipeline() -> Any:
global pipeline
if pipeline is not None:
return pipeline
with pipeline_lock:
if pipeline is not None:
return pipeline
kwargs: dict[str, Any] = {"torch_dtype": torch_dtype()}
if CACHE_DIR:
kwargs["cache_dir"] = CACHE_DIR
loaded = StableVideoDiffusionPipeline.from_pretrained(MODEL_ID, **kwargs)
if ENABLE_CPU_OFFLOAD:
loaded.enable_model_cpu_offload()
else:
loaded.to(DEVICE)
pipeline = loaded
return loaded
def positive_int(name: str, value: Any, default: int) -> int:
if value in (None, ""):
return default
try:
parsed = int(value)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{name} must be an integer") from exc
if parsed <= 0:
raise HTTPException(status_code=400, detail=f"{name} must be greater than zero")
return parsed
def optional_int(name: str, value: Any) -> int | None:
if value in (None, ""):
return None
try:
return int(value)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{name} must be an integer") from exc
def optional_float(name: str, value: Any) -> float | None:
if value in (None, ""):
return None
try:
return float(value)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{name} must be a number") from exc
def load_image_from_path(input_image_path: str) -> Any:
path = Path(input_image_path).expanduser()
if not path.exists() or not path.is_file():
raise HTTPException(status_code=400, detail="input_image_path does not exist or is not a file")
try:
return pil_image.open(path).convert("RGB")
except (OSError, pil_unidentified_image_error) as exc:
raise HTTPException(status_code=400, detail="input_image_path could not be decoded as an image") from exc
async def load_uploaded_image(image: UploadFile) -> Any:
try:
return pil_image.open(image.file).convert("RGB")
except (OSError, pil_unidentified_image_error) as exc:
raise HTTPException(status_code=400, detail="uploaded image could not be decoded") from exc
async def json_payload(request: Request) -> dict[str, Any]:
content_type = request.headers.get("content-type", "")
if not content_type.startswith("application/json"):
return {}
try:
payload = await request.json()
except ValueError as exc:
raise HTTPException(status_code=400, detail="request body must be valid JSON") from exc
if not isinstance(payload, dict):
raise HTTPException(status_code=400, detail="JSON request body must be an object")
return payload
@app.get("/health")
def health() -> dict[str, Any]:
return {
"status": "ok",
"model_loaded": pipeline is not None,
"model_id": MODEL_ID,
"device": DEVICE,
"dtype": DTYPE,
"cpu_offload": ENABLE_CPU_OFFLOAD,
"cache_dir": CACHE_DIR,
"output_dir": str(OUTPUT_DIR),
}
@app.post("/generate")
async def generate(
request: Request,
image: UploadFile | None = File(default=None),
input_image_path: str | None = Form(default=None),
num_frames: int | None = Form(default=None),
num_inference_steps: int | None = Form(default=None),
fps: int | None = Form(default=None),
decode_chunk_size: int | None = Form(default=None),
seed: int | None = Form(default=None),
motion_bucket_id: int | None = Form(default=None),
noise_aug_strength: float | None = Form(default=None),
) -> dict[str, Any]:
payload = await json_payload(request)
path = input_image_path or payload.get("input_image_path")
upload = image
if upload is None and not path:
raise HTTPException(status_code=400, detail="provide input_image_path or upload image")
if upload is not None and path:
raise HTTPException(status_code=400, detail="provide only one image source")
source_image = await load_uploaded_image(upload) if upload is not None else load_image_from_path(str(path))
chunk_size = positive_int("decode_chunk_size", decode_chunk_size if decode_chunk_size is not None else payload.get("decode_chunk_size"), DEFAULT_DECODE_CHUNK_SIZE)
output_fps = positive_int("fps", fps if fps is not None else payload.get("fps"), DEFAULT_FPS)
request_seed = optional_int("seed", seed if seed is not None else payload.get("seed"))
generator = None
if request_seed is not None:
generator = torch.Generator(device="cpu").manual_seed(request_seed)
call_args: dict[str, Any] = {
"image": source_image,
"decode_chunk_size": chunk_size,
}
for key, value in {
"num_frames": num_frames if num_frames is not None else payload.get("num_frames"),
"num_inference_steps": num_inference_steps if num_inference_steps is not None else payload.get("num_inference_steps"),
"motion_bucket_id": motion_bucket_id if motion_bucket_id is not None else payload.get("motion_bucket_id"),
}.items():
parsed = optional_int(key, value)
if parsed is not None:
call_args[key] = parsed
parsed_noise = optional_float("noise_aug_strength", noise_aug_strength if noise_aug_strength is not None else payload.get("noise_aug_strength"))
if parsed_noise is not None:
call_args["noise_aug_strength"] = parsed_noise
if generator is not None:
call_args["generator"] = generator
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
output_path = OUTPUT_DIR / f"svd-{uuid.uuid4().hex}.mp4"
try:
frames = get_pipeline()(**call_args).frames[0]
export_to_video(frames, str(output_path), fps=output_fps)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
return {
"output_path": str(output_path),
"model_id": MODEL_ID,
"seed": request_seed,
"fps": output_fps,
}
def main() -> None:
uvicorn.run("app:app", host=HOST, port=PORT)
if __name__ == "__main__":
main()
@@ -0,0 +1,41 @@
{ pkgs }:
let
pythonEnv = pkgs.python3.withPackages (ps: let
torch = ps.torchWithCuda;
accelerate = ps.accelerate.override { inherit torch; };
in [
accelerate
ps.diffusers
ps.fastapi
ps.imageio
ps.imageio-ffmpeg
ps.pillow
ps.python-multipart
ps.safetensors
torch
ps.transformers
ps.uvicorn
]);
in
pkgs.stdenv.mkDerivation {
pname = "stable-video-diffusion-api";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ pkgs.makeWrapper ];
installPhase = ''
mkdir -p $out/bin $out/libexec/stable-video-diffusion-api
cp $src/app.py $out/libexec/stable-video-diffusion-api/app.py
chmod +x $out/libexec/stable-video-diffusion-api/app.py
makeWrapper ${pythonEnv}/bin/python3 $out/bin/stable-video-diffusion-api \
--add-flags $out/libexec/stable-video-diffusion-api/app.py \
--set-default SVD_API_HOST 127.0.0.1 \
--set-default SVD_API_PORT 8000 \
--set-default SVD_MODEL_ID stabilityai/stable-video-diffusion-img2vid-xt \
--set-default SVD_OUTPUT_DIR /var/lib/stable-video-diffusion-api/outputs
'';
}
+1939
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "which-country-rs"
version = "1.0.0"
edition = "2024"
description = "CLI tool that detects your country by IP and renders an ASCII map"
license = "MIT"
repository = "https://github.com/krisfur/which-country-rs"
keywords = ["cli", "geolocation", "ascii", "map"]
categories = ["command-line-utilities"]
[features]
default = ["geoip"]
geoip = ["reqwest"]
[dependencies]
clap = { version = "4", features = ["derive"] }
reqwest = { version = "0.13", features = ["blocking", "json"], optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Krzysztof Furman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+83
View File
@@ -0,0 +1,83 @@
# which-country-rs
A CLI tool that detects your country from your IP address and renders an ASCII map zoomed into it, showing neighbouring borders and country codes.
## Example output
```bash
which-country-rs -c DE
```
```
You appear to be in: Germany (DE)
·········· ·· ······ · ·
· NO · ·· · ···
· · ···· ··· ···········
· ····· ·· ············
····· ········ · ·· ···EE ·
· ······ ······ ·· · LV····· ·····
·· ··· ·DK ·· ··· ···· ··········· ···
···· ··· ··· · · ··· ···· ··· LT ···· ···
· ····· ···· ··· ############ ··················· ···
IE ·· ····GB · ·····##### ## ## ···· BY
· ·· ·· ··· ·NL # ## ··
··· ····· ·· ····· ## # PL ·············
····················BE ··# DE #######····· ····
· ······· ···L## ### CZ ····· ······
······· #### ###·········SK ······· ··
····· ·###### ###### AT ········ ···············
·· FR ··· CH ··#·············· HU ·· MD··
· · ····· · ····SI··· ········· RO ····
· · · ···HR········ RS···· ···
········ ·· · ········ IT ··· ·····BA ·· ··············
· ·· ········· ·········· ·· · · ···ME··XK···· ··
····· ··· · ·· · ··· ···· ·· ··MK·· ·BG ·····
· ·· ····· ··· ····· ···· ·AL············ ······
· ·· ES · · · ·· ····· ···· ···· ······
Coordinates: 51.15°N, 10.55°E
```
## Usage
```
# Auto-detect from IP
which-country-rs
# Specify a country code
which-country-rs -c JP
# Specify coordinates (supports negative values for south/west)
which-country-rs --lat 40 --lon -74
# Custom map size
which-country-rs -W 120 -H 40
```
### Options
| Flag | Description | Default |
|------|-------------|---------|
| `-W, --width` | Map width in characters | 80 |
| `-H, --height` | Map height in characters | 24 |
| `-c, --country` | ISO 3166-1 alpha-2 country code (skips IP lookup) | |
| `--lat` | Latitude (requires `--lon`) | |
| `--lon` | Longitude (requires `--lat`) | |
| `-V, --version` | Print version | |
## Building
```
cargo build --release
```
To build without IP geolocation support (drops the `reqwest` dependency):
```
cargo build --release --no-default-features
```
## Map data
Country borders from [Natural Earth](https://www.naturalearthdata.com/) 110m admin-0 countries (public domain).
File diff suppressed because one or more lines are too long
+25
View File
@@ -0,0 +1,25 @@
{ lib, pkgs, ... }: let
src = ./.;
in
pkgs.rustPlatform.buildRustPackage {
pname = "which-country-rs";
version = "1.0.0";
inherit src;
cargoLock.lockFile = ./Cargo.lock;
# NOTE(yukkop): upstream uses edition 2024, so do not pass the repo's
# Rust 1.81 commonArgs here.
# NOTE(yukkop): keep the Nix package offline-by-default; upstream's
# default geoip feature calls a plaintext HTTP endpoint.
cargoBuildFlags = ["--no-default-features"];
cargoTestFlags = ["--no-default-features"];
meta = {
description = "CLI tool that tells you which country contains coordinates";
homepage = "https://github.com/krisfur/which-country-rs";
license = lib.licenses.mit;
mainProgram = "which-country-rs";
};
}
+275
View File
@@ -0,0 +1,275 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct FeatureCollection {
features: Vec<Feature>,
}
#[derive(Debug, Deserialize)]
struct Feature {
properties: Properties,
geometry: Geometry,
}
#[derive(Debug, Deserialize)]
struct Properties {
#[serde(rename = "ISO_A2_EH")]
iso_a2: String,
#[serde(rename = "NAME")]
name: String,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
enum Geometry {
Polygon {
coordinates: Vec<Vec<[f64; 2]>>,
},
MultiPolygon {
coordinates: Vec<Vec<Vec<[f64; 2]>>>,
},
}
#[allow(dead_code)]
pub struct Country {
pub iso_a2: String,
pub name: String,
/// Each polygon is a list of rings; ring 0 = outer, rest = holes
pub polygons: Vec<Vec<Vec<[f64; 2]>>>,
pub bbox: (f64, f64, f64, f64), // (min_lon, min_lat, max_lon, max_lat)
pub label_pos: (f64, f64), // (lon, lat) — centroid of largest polygon
}
/// Signed area of a ring (positive = CCW).
fn ring_signed_area(ring: &[[f64; 2]]) -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let mut area = 0.0;
let mut j = n - 1;
for i in 0..n {
area += (ring[j][0] - ring[i][0]) * (ring[j][1] + ring[i][1]);
j = i;
}
area / 2.0
}
/// Ray-casting point-in-ring test.
fn point_in_ring(lon: f64, lat: f64, ring: &[[f64; 2]]) -> bool {
let mut inside = false;
let n = ring.len();
if n < 3 {
return false;
}
let mut j = n - 1;
for i in 0..n {
let xi = ring[i][0];
let yi = ring[i][1];
let xj = ring[j][0];
let yj = ring[j][1];
if ((yi > lat) != (yj > lat)) && (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) {
inside = !inside;
}
j = i;
}
inside
}
/// Find horizontal interior spans at a given latitude.
/// Returns sorted pairs of (enter_lon, exit_lon).
fn horizontal_spans(lat: f64, ring: &[[f64; 2]]) -> Vec<(f64, f64)> {
let n = ring.len();
if n < 3 {
return Vec::new();
}
let mut crossings = Vec::new();
let mut j = n - 1;
for i in 0..n {
let yi = ring[i][1];
let yj = ring[j][1];
if (yi > lat) != (yj > lat) {
let xi = ring[i][0];
let xj = ring[j][0];
crossings.push((xj - xi) * (lat - yi) / (yj - yi) + xi);
}
j = i;
}
crossings.sort_by(|a, b| a.partial_cmp(b).unwrap());
crossings.chunks_exact(2).map(|p| (p[0], p[1])).collect()
}
/// Find vertical interior spans at a given longitude.
/// Returns sorted pairs of (enter_lat, exit_lat).
fn vertical_spans(lon: f64, ring: &[[f64; 2]]) -> Vec<(f64, f64)> {
let n = ring.len();
if n < 3 {
return Vec::new();
}
let mut crossings = Vec::new();
let mut j = n - 1;
for i in 0..n {
let xi = ring[i][0];
let xj = ring[j][0];
if (xi > lon) != (xj > lon) {
let yi = ring[i][1];
let yj = ring[j][1];
crossings.push((yj - yi) * (lon - xi) / (xj - xi) + yi);
}
j = i;
}
crossings.sort_by(|a, b| a.partial_cmp(b).unwrap());
crossings.chunks_exact(2).map(|p| (p[0], p[1])).collect()
}
/// Find a good interior label point for a ring.
/// Scans a grid of candidate points and picks the one that maximizes
/// min(half_width, half_height) — the "most interior" point.
fn ring_label_point(ring: &[[f64; 2]]) -> (f64, f64) {
let min_lon = ring.iter().map(|c| c[0]).fold(f64::MAX, f64::min);
let max_lon = ring.iter().map(|c| c[0]).fold(f64::MIN, f64::max);
let min_lat = ring.iter().map(|c| c[1]).fold(f64::MAX, f64::min);
let max_lat = ring.iter().map(|c| c[1]).fold(f64::MIN, f64::max);
let steps = 24;
let mut best = ((min_lon + max_lon) / 2.0, (min_lat + max_lat) / 2.0);
let mut best_score = 0.0f64;
for row in 1..steps {
let lat = min_lat + (max_lat - min_lat) * row as f64 / steps as f64;
let h_spans = horizontal_spans(lat, ring);
for &(span_left, span_right) in &h_spans {
let mid_lon = (span_left + span_right) / 2.0;
let half_w = (span_right - span_left) / 2.0;
// Measure vertical extent at this longitude
let v_spans = vertical_spans(mid_lon, ring);
for &(span_bot, span_top) in &v_spans {
if lat >= span_bot && lat <= span_top {
let half_h = ((lat - span_bot).min(span_top - lat)).min(half_w);
let score = half_w.min(half_h);
if score > best_score {
best_score = score;
best = (mid_lon, lat);
}
break;
}
}
}
}
best
}
pub fn load_countries(geojson: &str) -> Vec<Country> {
let fc: FeatureCollection = serde_json::from_str(geojson).expect("Failed to parse GeoJSON");
fc.features
.into_iter()
.map(|f| {
let polygons = match f.geometry {
Geometry::Polygon { coordinates } => vec![coordinates],
Geometry::MultiPolygon { coordinates } => coordinates,
};
let mut min_lon = f64::MAX;
let mut min_lat = f64::MAX;
let mut max_lon = f64::MIN;
let mut max_lat = f64::MIN;
for poly in &polygons {
for ring in poly {
for coord in ring {
let lon = coord[0];
let lat = coord[1];
if lon < min_lon {
min_lon = lon;
}
if lon > max_lon {
max_lon = lon;
}
if lat < min_lat {
min_lat = lat;
}
if lat > max_lat {
max_lat = lat;
}
}
}
}
// Label inside the largest polygon
let label_pos = polygons
.iter()
.filter(|p| !p.is_empty() && p[0].len() >= 3)
.max_by(|a, b| {
ring_signed_area(&a[0])
.abs()
.partial_cmp(&ring_signed_area(&b[0]).abs())
.unwrap()
})
.map(|p| ring_label_point(&p[0]))
.unwrap_or(((min_lon + max_lon) / 2.0, (min_lat + max_lat) / 2.0));
Country {
iso_a2: f.properties.iso_a2,
name: f.properties.name,
polygons,
bbox: (min_lon, min_lat, max_lon, max_lat),
label_pos,
}
})
.collect()
}
/// Check if a point is inside a polygon (outer ring minus holes).
fn point_in_polygon(lon: f64, lat: f64, rings: &[Vec<[f64; 2]>]) -> bool {
if rings.is_empty() || !point_in_ring(lon, lat, &rings[0]) {
return false;
}
// Must be outside all holes
!rings[1..].iter().any(|hole| point_in_ring(lon, lat, hole))
}
/// Check if a point falls inside a country.
pub fn point_in_country(lon: f64, lat: f64, country: &Country) -> bool {
let (min_lon, min_lat, max_lon, max_lat) = country.bbox;
if lon < min_lon || lon > max_lon || lat < min_lat || lat > max_lat {
return false;
}
country
.polygons
.iter()
.any(|poly| point_in_polygon(lon, lat, poly))
}
/// Find which country contains the given point, with a nearest-country
/// fallback for when low-res coastlines cause a near miss.
pub fn find_country(lon: f64, lat: f64, countries: &[Country]) -> Option<usize> {
// Exact hit
if let Some(idx) = countries.iter().position(|c| point_in_country(lon, lat, c)) {
return Some(idx);
}
// Search in expanding rings up to ~1 degree
for &offset in &[0.25, 0.5, 1.0] {
for &(dlon, dlat) in &[
(offset, 0.0),
(-offset, 0.0),
(0.0, offset),
(0.0, -offset),
(offset, offset),
(offset, -offset),
(-offset, offset),
(-offset, -offset),
] {
if let Some(idx) = countries
.iter()
.position(|c| point_in_country(lon + dlon, lat + dlat, c))
{
return Some(idx);
}
}
}
None
}
+44
View File
@@ -0,0 +1,44 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(tag = "status")]
pub enum GeoIpResponse {
#[serde(rename = "success")]
Success {
country: String,
#[serde(rename = "countryCode")]
country_code: String,
lat: f64,
lon: f64,
},
#[serde(rename = "fail")]
Fail { message: String },
}
pub struct GeoIpResult {
pub country: String,
pub country_code: String,
pub lat: f64,
pub lon: f64,
}
pub fn lookup() -> Result<GeoIpResult, String> {
let url = "http://ip-api.com/json/?fields=status,message,countryCode,country,lat,lon";
let resp = reqwest::blocking::get(url).map_err(|e| format!("HTTP request failed: {e}"))?;
let data: GeoIpResponse = resp.json().map_err(|e| format!("Failed to parse response: {e}"))?;
match data {
GeoIpResponse::Success {
country,
country_code,
lat,
lon,
} => Ok(GeoIpResult {
country,
country_code,
lat,
lon,
}),
GeoIpResponse::Fail { message } => Err(format!("GeoIP lookup failed: {message}")),
}
}
+99
View File
@@ -0,0 +1,99 @@
mod geo;
#[cfg(feature = "geoip")]
mod geoip;
mod render;
use clap::Parser;
#[derive(Parser)]
#[command(name = "which-country-rs", version, about = "Detect your country by IP and render an ASCII world map")]
struct Args {
/// Map width in characters
#[arg(short = 'W', long, default_value_t = 80)]
width: usize,
/// Map height in characters
#[arg(short = 'H', long, default_value_t = 24)]
height: usize,
/// Country code to display (e.g. "US", "FR", "JP") — skips IP lookup
#[arg(short, long)]
country: Option<String>,
/// Latitude (requires --lon too) — skips IP lookup, derives country from coordinates
#[arg(long, requires = "lon", allow_hyphen_values = true)]
lat: Option<f64>,
/// Longitude (requires --lat too)
#[arg(long, requires = "lat", allow_hyphen_values = true)]
lon: Option<f64>,
}
static GEOJSON: &str = include_str!("../data/countries.geojson");
fn main() {
let args = Args::parse();
let countries = geo::load_countries(GEOJSON);
let (country_name, country_code, lat, lon) = if let Some(code) = &args.country {
// Direct country code — find it in the data
let code_upper = code.to_uppercase();
let c = countries
.iter()
.find(|c| c.iso_a2 == code_upper)
.unwrap_or_else(|| {
eprintln!("Unknown country code: {code_upper}");
std::process::exit(1);
});
let (label_lon, label_lat) = c.label_pos;
(c.name.clone(), code_upper, label_lat, label_lon)
} else if let (Some(lat), Some(lon)) = (args.lat, args.lon) {
// Coordinates provided — find which country contains the point
let idx = geo::find_country(lon, lat, &countries)
.unwrap_or_else(|| {
eprintln!("No country found at {lat}, {lon} (ocean?)");
std::process::exit(1);
});
(
countries[idx].name.clone(),
countries[idx].iso_a2.clone(),
lat,
lon,
)
} else {
// IP geolocation
#[cfg(feature = "geoip")]
{
eprint!("Looking up your location... ");
match geoip::lookup() {
Ok(loc) => {
eprintln!("done.");
(loc.country, loc.country_code, loc.lat, loc.lon)
}
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
}
}
#[cfg(not(feature = "geoip"))]
{
eprintln!("IP geolocation requires the 'geoip' feature. Use --country or --lat/--lon instead.");
std::process::exit(1);
}
};
let map = render::render_map(&countries, &country_code, args.width, args.height);
println!("You appear to be in: {country_name} ({country_code})");
println!();
println!("{map}");
println!();
println!(
"Coordinates: {:.2}\u{00b0}{}, {:.2}\u{00b0}{}",
lat.abs(),
if lat >= 0.0 { "N" } else { "S" },
lon.abs(),
if lon >= 0.0 { "E" } else { "W" },
);
}
+184
View File
@@ -0,0 +1,184 @@
use crate::geo::Country;
/// Render a zoomed-in ASCII map centered on the target country, showing borders and labels.
pub fn render_map(
countries: &[Country],
target_code: &str,
width: usize,
height: usize,
) -> String {
// Find the target country and compute its bbox
let target_idx = countries
.iter()
.position(|c| c.iso_a2 == target_code)
.expect("Target country not found in GeoJSON");
let (min_lon, min_lat, max_lon, max_lat) = countries[target_idx].bbox;
// Add generous padding so the surrounding continent is visible
let lon_span = (max_lon - min_lon).max(4.0);
let lat_span = (max_lat - min_lat).max(4.0);
let pad_lon = lon_span * 1.0;
let pad_lat = lat_span * 1.0;
let view_min_lon = (min_lon - pad_lon).max(-180.0);
let view_max_lon = (max_lon + pad_lon).min(180.0);
let view_min_lat = (min_lat - pad_lat).max(-90.0);
let view_max_lat = (max_lat + pad_lat).min(90.0);
// Adjust aspect ratio: terminal chars are ~2x taller than wide
let lon_range = view_max_lon - view_min_lon;
let lat_range = view_max_lat - view_min_lat;
let char_aspect = 2.0;
let (final_lon_range, final_lat_range);
let desired_lon = lat_range * (width as f64) / (height as f64) / char_aspect;
let desired_lat = lon_range * (height as f64) / (width as f64) * char_aspect;
if desired_lon > lon_range {
final_lon_range = desired_lon;
final_lat_range = lat_range;
} else {
final_lon_range = lon_range;
final_lat_range = desired_lat;
}
let center_lon = (view_min_lon + view_max_lon) / 2.0;
let center_lat = (view_min_lat + view_max_lat) / 2.0;
let vp_min_lon = (center_lon - final_lon_range / 2.0).max(-180.0);
let vp_max_lat = (center_lat + final_lat_range / 2.0).min(90.0);
let lon_per_col = final_lon_range / width as f64;
let lat_per_row = final_lat_range / height as f64;
// Grid of characters
let mut grid = vec![vec![' '; width]; height];
// Rasterize polygon edges onto the grid
for (i, country) in countries.iter().enumerate() {
let (c_min_lon, c_min_lat, c_max_lon, c_max_lat) = country.bbox;
if c_max_lon < vp_min_lon
|| c_min_lon > vp_min_lon + final_lon_range
|| c_max_lat < vp_max_lat - final_lat_range
|| c_min_lat > vp_max_lat
{
continue;
}
let border_ch = if i == target_idx { '#' } else { '\u{00b7}' };
for poly in &country.polygons {
for ring in poly {
if ring.len() < 2 {
continue;
}
for edge in ring.windows(2) {
rasterize_edge(
edge[0][0],
edge[0][1],
edge[1][0],
edge[1][1],
vp_min_lon,
vp_max_lat,
lon_per_col,
lat_per_row,
width,
height,
border_ch,
i == target_idx,
&mut grid,
);
}
}
}
}
// Place country code labels at bbox center
for (i, country) in countries.iter().enumerate() {
if country.iso_a2 == "-99" {
continue;
}
let (label_lon, label_lat) = country.label_pos;
let col = ((label_lon - vp_min_lon) / lon_per_col) as isize;
let row = ((vp_max_lat - label_lat) / lat_per_row) as isize;
let label = &country.iso_a2;
let start_col = col - (label.len() as isize / 2);
for (j, ch) in label.chars().enumerate() {
let c = start_col + j as isize;
if c >= 0 && (c as usize) < width && row >= 0 && (row as usize) < height {
let r = row as usize;
let c = c as usize;
let existing = grid[r][c];
// Target label always writes; neighbor labels only on empty or neighbor border
if i == target_idx || existing == ' ' || existing == '\u{00b7}' {
grid[r][c] = ch;
}
}
}
}
// Render grid to string
grid.iter()
.map(|row| row.iter().collect::<String>())
.collect::<Vec<_>>()
.join("\n")
}
/// Rasterize a line segment onto the grid using Bresenham's algorithm.
fn rasterize_edge(
lon0: f64,
lat0: f64,
lon1: f64,
lat1: f64,
vp_min_lon: f64,
vp_max_lat: f64,
lon_per_col: f64,
lat_per_row: f64,
width: usize,
height: usize,
ch: char,
is_target: bool,
grid: &mut [Vec<char>],
) {
let c0 = ((lon0 - vp_min_lon) / lon_per_col) as i32;
let r0 = ((vp_max_lat - lat0) / lat_per_row) as i32;
let c1 = ((lon1 - vp_min_lon) / lon_per_col) as i32;
let r1 = ((vp_max_lat - lat1) / lat_per_row) as i32;
let mut x = c0;
let mut y = r0;
let dx = (c1 - c0).abs();
let dy = -(r1 - r0).abs();
let sx = if c0 < c1 { 1 } else { -1 };
let sy = if r0 < r1 { 1 } else { -1 };
let mut err = dx + dy;
loop {
if x >= 0 && (x as usize) < width && y >= 0 && (y as usize) < height {
let cell = &mut grid[y as usize][x as usize];
// Target borders overwrite neighbor borders; neighbor borders only on empty
if is_target || *cell == ' ' {
*cell = ch;
}
}
if x == c1 && y == r1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x += sx;
}
if e2 <= dx {
err += dx;
y += sy;
}
}
}
@@ -24,6 +24,14 @@ if [ "$answer" -ne 4 ]; then
exit 1
fi
log notice "test case: ${WHITE}unguarded assignment under errexit"
answer=$(index_of "$array" 'item4')
if [ "$answer" -ne 4 ]; then
log error "test failed: ${WHITE}wrong answer from unguarded assignment"
exit 1
fi
log notice "test case: ${WHITE}error: item not found"
if answer=$(index_of "$array" 'item10'); then
log error "test failed: ${WHITE} must return an error"