Compare commits
11 Commits
f73bfc63be
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 557b6e9ef0 | |||
| e49f497045 | |||
| feb1a48db1 | |||
| 30732080b7 | |||
| 80cf1588bb | |||
| ef7d1b29f4 | |||
| 7e8c6884db | |||
| 1dd41e608b | |||
| e41c3e5a05 | |||
| f473280bf5 | |||
| b08fdd6e6b |
@@ -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
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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
@@ -688,6 +688,27 @@
|
|||||||
"url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git"
|
"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": {
|
"nix-minecraft": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat_3",
|
"flake-compat": "flake-compat_3",
|
||||||
@@ -967,6 +988,7 @@
|
|||||||
"hyprland": "hyprland",
|
"hyprland": "hyprland",
|
||||||
"impermanence": "impermanence",
|
"impermanence": "impermanence",
|
||||||
"mechabellum-replay-analysis": "mechabellum-replay-analysis",
|
"mechabellum-replay-analysis": "mechabellum-replay-analysis",
|
||||||
|
"nix-darwin": "nix-darwin",
|
||||||
"nix-minecraft": "nix-minecraft",
|
"nix-minecraft": "nix-minecraft",
|
||||||
"nixos-anywhere": "nixos-anywhere",
|
"nixos-anywhere": "nixos-anywhere",
|
||||||
"nixos-hardware": "nixos-hardware",
|
"nixos-hardware": "nixos-hardware",
|
||||||
|
|||||||
@@ -42,6 +42,10 @@
|
|||||||
url = "github:nix-community/home-manager/release-25.11";
|
url = "github:nix-community/home-manager/release-25.11";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
nix-darwin = {
|
||||||
|
url = "github:nix-darwin/nix-darwin/nix-darwin-25.11";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
nixos-wsl = {
|
nixos-wsl = {
|
||||||
url = "github:nix-community/NixOS-WSL";
|
url = "github:nix-community/NixOS-WSL";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
@@ -123,5 +127,8 @@
|
|||||||
"tenix|x86_64-linux" = import ./nixos/system/tenix { inherit flake self inputs; system = "x86_64-linux"; };
|
"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"; };
|
"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"; };
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -26,12 +26,39 @@
|
|||||||
"aarch64-darwin"
|
"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:
|
forSystemsWithPkgs = supportedSystems: pkgOverlays: f:
|
||||||
builtins.foldl' (
|
builtins.foldl' (
|
||||||
acc: system: let
|
acc: system: let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
overlays = pkgOverlays;
|
overlays = pkgOverlays;
|
||||||
|
config.allowUnfreePredicate = cudaUnfreePredicate;
|
||||||
};
|
};
|
||||||
systemOutputs = f {
|
systemOutputs = f {
|
||||||
system = system;
|
system = system;
|
||||||
@@ -58,7 +85,7 @@
|
|||||||
else {};
|
else {};
|
||||||
in {
|
in {
|
||||||
# -- For all systems --
|
# -- 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;
|
forSystems = systems: nixpkgs.lib.genAttrs systems;
|
||||||
forAllSystems = nixpkgs.lib.genAttrs AllSystems;
|
forAllSystems = nixpkgs.lib.genAttrs AllSystems;
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ in {
|
|||||||
# failover flip does not need a separate provisioning step.
|
# failover flip does not need a separate provisioning step.
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d /var/lib/matrix-synapse 0750 matrix-synapse matrix-synapse -"
|
"d /var/lib/matrix-synapse 0750 matrix-synapse matrix-synapse -"
|
||||||
|
"Z ${s3Cfg.mediaStorePath} 0700 matrix-synapse matrix-synapse -"
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.matrix-cluster-signing-key = {
|
systemd.services.matrix-cluster-signing-key = {
|
||||||
|
|||||||
@@ -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 = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -25,36 +25,7 @@ in {
|
|||||||
programs.bash.shellAliases.tmux = "tmux a";
|
programs.bash.shellAliases.tmux = "tmux a";
|
||||||
|
|
||||||
home-manager.sharedModules = [
|
home-manager.sharedModules = [
|
||||||
{
|
(flake + "/home/module/program/tmux.nix")
|
||||||
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 :.-
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
home-manager.users.root.home.stateVersion = lib.mkDefault "25.05";
|
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,
|
config,
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
matrixBackend = "https://128.140.75.58";
|
|
||||||
matrixHost = "accord.tube";
|
matrixHost = "accord.tube";
|
||||||
jitsiHost = "meet.accord.tube";
|
jitsiHost = "meet.accord.tube";
|
||||||
elementEntryDomain = "element.bfs.band";
|
elementEntryDomain = "element.bfs.band";
|
||||||
polandEntryDomain = "bfs.band";
|
polandEntryDomain = "bfs.band";
|
||||||
matrixClusterSopsFile = flake + "/sus/matrix-cluster.yaml";
|
matrixClusterSopsFile = flake + "/sus/matrix-cluster.yaml";
|
||||||
backendProxyConfig = ''
|
|
||||||
proxy_ssl_server_name on;
|
|
||||||
proxy_ssl_name ${matrixHost};
|
|
||||||
proxy_set_header Host ${matrixHost};
|
|
||||||
'';
|
|
||||||
in {
|
in {
|
||||||
imports = [
|
imports = [
|
||||||
self.nixosModules.xray-system
|
self.nixosModules.xray-system
|
||||||
@@ -47,7 +41,6 @@ in {
|
|||||||
credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
|
credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
|
||||||
};
|
};
|
||||||
replication = {
|
replication = {
|
||||||
peerHost = "128.140.75.58";
|
|
||||||
passwordFile = config.sops.secrets."matrix/postgres-replication-password".path;
|
passwordFile = config.sops.secrets."matrix/postgres-replication-password".path;
|
||||||
};
|
};
|
||||||
acme = {
|
acme = {
|
||||||
@@ -133,19 +126,17 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
locations."= /livekit/jwt" = {
|
locations."= /livekit/jwt" = {
|
||||||
proxyPass = "${matrixBackend}/livekit/jwt";
|
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/";
|
||||||
extraConfig = backendProxyConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
locations."^~ /livekit/jwt/" = {
|
locations."^~ /livekit/jwt/" = {
|
||||||
proxyPass = "${matrixBackend}/livekit/jwt/";
|
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/";
|
||||||
extraConfig = backendProxyConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
locations."= /livekit/sfu" = {
|
locations."= /livekit/sfu" = {
|
||||||
proxyPass = "${matrixBackend}/livekit/sfu";
|
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
extraConfig = backendProxyConfig + ''
|
extraConfig = ''
|
||||||
proxy_send_timeout 120;
|
proxy_send_timeout 120;
|
||||||
proxy_read_timeout 120;
|
proxy_read_timeout 120;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
@@ -156,9 +147,9 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
locations."^~ /livekit/sfu/" = {
|
locations."^~ /livekit/sfu/" = {
|
||||||
proxyPass = "${matrixBackend}/livekit/sfu/";
|
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
extraConfig = backendProxyConfig + ''
|
extraConfig = ''
|
||||||
proxy_send_timeout 120;
|
proxy_send_timeout 120;
|
||||||
proxy_read_timeout 120;
|
proxy_read_timeout 120;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
@@ -169,13 +160,14 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
locations."^~ /_matrix/" = {
|
locations."^~ /_matrix/" = {
|
||||||
proxyPass = "${matrixBackend}/_matrix/";
|
proxyPass = "http://127.0.0.1:8008";
|
||||||
extraConfig = backendProxyConfig;
|
extraConfig = ''
|
||||||
|
client_max_body_size ${config.hectic.generic.matrix-cluster.maxUploadSize};
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
locations."^~ /_synapse/client/" = {
|
locations."^~ /_synapse/client/" = {
|
||||||
proxyPass = "${matrixBackend}/_synapse/client/";
|
proxyPass = "http://127.0.0.1:8008";
|
||||||
extraConfig = backendProxyConfig;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ in {
|
|||||||
|
|
||||||
(import ./attic.nix { inherit flake self inputs domain; })
|
(import ./attic.nix { inherit flake self inputs domain; })
|
||||||
(import ./containers.nix { inherit flake self inputs; })
|
(import ./containers.nix { inherit flake self inputs; })
|
||||||
|
./experimental-sshd.nix
|
||||||
(import ./ente.nix { inherit domain; })
|
(import ./ente.nix { inherit domain; })
|
||||||
(import ./mechabellum.nix { inherit flake self inputs domain; })
|
(import ./mechabellum.nix { inherit flake self inputs domain; })
|
||||||
(import (./. + "/sentinèlla.nix") { inherit flake self inputs domain; })
|
(import (./. + "/sentinèlla.nix") { inherit flake self inputs domain; })
|
||||||
|
|||||||
@@ -15,32 +15,12 @@ in self.lib.nixpkgs-lib.nixosSystem {
|
|||||||
self.overlays.default
|
self.overlays.default
|
||||||
inputs.nix-minecraft.overlay
|
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"
|
"minecraft-server"
|
||||||
"neoforge"
|
"neoforge"
|
||||||
|
|
||||||
"nvidia-x11"
|
"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)
|
# jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x)
|
||||||
config.permittedInsecurePackages = [
|
config.permittedInsecurePackages = [
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
|
|
||||||
ollamaServiceBundledLibraryPath = "${ollamaPrebuilt}/lib/ollama:${ollamaPrebuilt}/lib/ollama/cuda_v12:${ollamaPrebuilt}/lib/ollama/cuda_v13";
|
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 {
|
ollamaPrebuilt = pkgs.stdenvNoCC.mkDerivation {
|
||||||
pname = "ollama";
|
pname = "ollama";
|
||||||
version = "0.22.1";
|
version = "0.22.1";
|
||||||
@@ -176,6 +181,19 @@ in {
|
|||||||
openFirewall = false;
|
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 = {
|
networking = {
|
||||||
networkmanager.enable = true;
|
networkmanager.enable = true;
|
||||||
useDHCP = lib.mkDefault true;
|
useDHCP = lib.mkDefault true;
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ in {
|
|||||||
github-gh-tl = pkgs.callPackage ./github/gh-tl.nix {};
|
github-gh-tl = pkgs.callPackage ./github/gh-tl.nix {};
|
||||||
supabase-with-env-collection = pkgs.callPackage ./supabase-with-env-collection.nix {};
|
supabase-with-env-collection = pkgs.callPackage ./supabase-with-env-collection.nix {};
|
||||||
migration-name = pkgs.callPackage ./migration-name.nix {};
|
migration-name = pkgs.callPackage ./migration-name.nix {};
|
||||||
|
plgo = pkgs.callPackage ./plgo {};
|
||||||
pg-from = pkgs.callPackage ./postgres/pg-from/default.nix rust.commonArgs;
|
pg-from = pkgs.callPackage ./postgres/pg-from/default.nix rust.commonArgs;
|
||||||
pg-schema = pkgs.callPackage ./postgres/pg-schema/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;
|
pg_wdumpall = pkgs.callPackage ./postgres/pg_wdumpall.nix rust.commonArgs;
|
||||||
@@ -148,6 +149,7 @@ in {
|
|||||||
deploy = pkgs.callPackage ./deploy { inherit inputs; };
|
deploy = pkgs.callPackage ./deploy { inherit inputs; };
|
||||||
element-web = pkgs.callPackage ./element-web {};
|
element-web = pkgs.callPackage ./element-web {};
|
||||||
shellplot = pkgs.callPackage ./shellplot {};
|
shellplot = pkgs.callPackage ./shellplot {};
|
||||||
|
which-country-rs = pkgs.callPackage ./which-country-rs {};
|
||||||
onlinepubs2man = pkgs.callPackage ./onlinepubs2man {};
|
onlinepubs2man = pkgs.callPackage ./onlinepubs2man {};
|
||||||
migrator = pkgs.callPackage ./migrator { inherit self; };
|
migrator = pkgs.callPackage ./migrator { inherit self; };
|
||||||
"parse-uri" = pkgs.callPackage ./parse-uri {};
|
"parse-uri" = pkgs.callPackage ./parse-uri {};
|
||||||
@@ -174,5 +176,6 @@ in {
|
|||||||
pg-15-ext-smtp-client = buildSmtpExt pkgs "15";
|
pg-15-ext-smtp-client = buildSmtpExt pkgs "15";
|
||||||
pg-15-ext-plhaskell = buildPlHaskellExt pkgs "15";
|
pg-15-ext-plhaskell = buildPlHaskellExt pkgs "15";
|
||||||
pg-15-ext-plsh = buildPlShExt 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 {};
|
media-browser = pkgs.callPackage ./media-browser {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -434,6 +434,8 @@ migrate_down() {
|
|||||||
target_migration=$(printf '%s' "$fs_migrations" | sed -n "${target_line}p")
|
target_migration=$(printf '%s' "$fs_migrations" | sed -n "${target_line}p")
|
||||||
printf '%s' "$target_migration"
|
printf '%s' "$target_migration"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate_up() {
|
migrate_up() {
|
||||||
@@ -488,6 +490,7 @@ migrate_up() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s' "$target_migration"
|
printf '%s' "$target_migration"
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate_to() {
|
migrate_to() {
|
||||||
@@ -518,6 +521,8 @@ migrate_to() {
|
|||||||
printf '%s' "$migration_name"
|
printf '%s' "$migration_name"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
migration_list() {
|
migration_list() {
|
||||||
@@ -566,7 +571,10 @@ index_of() {
|
|||||||
|
|
||||||
# no subshell, no pipeline
|
# no subshell, no pipeline
|
||||||
while IFS= read -r m; do
|
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))
|
i=$((i+1))
|
||||||
done <<EOF
|
done <<EOF
|
||||||
$list
|
$list
|
||||||
@@ -697,14 +705,20 @@ migrate() {
|
|||||||
log info "Migration history validation: ${GREEN}OK${NC} (${WHITE}$i${NC} migrations match)"
|
log info "Migration history validation: ${GREEN}OK${NC} (${WHITE}$i${NC} migrations match)"
|
||||||
|
|
||||||
eval "set -- $MIGRATOR_REMAINING_ARS"
|
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
|
if [ -z "$db_migrations" ]; then
|
||||||
log info "starting from clean migration state"
|
log info "starting from clean migration state"
|
||||||
current_idx=0
|
current_idx=0
|
||||||
else
|
else
|
||||||
current_migration=$(printf '%s\n' "$db_migrations" | tail -n1)
|
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
|
fi
|
||||||
|
|
||||||
log debug "filesystem migrations: ${WHITE}$fs_migrations${NC}"
|
log debug "filesystem migrations: ${WHITE}$fs_migrations${NC}"
|
||||||
@@ -713,7 +727,10 @@ migrate() {
|
|||||||
if [ -z "$target_migration" ]; then
|
if [ -z "$target_migration" ]; then
|
||||||
target_idx=0
|
target_idx=0
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
log debug "migration indexes: current=${WHITE}$current_idx${NC} target=${WHITE}$target_idx${NC}"
|
log debug "migration indexes: current=${WHITE}$current_idx${NC} target=${WHITE}$target_idx${NC}"
|
||||||
|
|||||||
@@ -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";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
'';
|
||||||
|
}
|
||||||
Generated
+1939
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" },
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
exit 1
|
||||||
fi
|
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"
|
log notice "test case: ${WHITE}error: item not found"
|
||||||
if answer=$(index_of "$array" 'item10'); then
|
if answer=$(index_of "$array" 'item10'); then
|
||||||
log error "test failed: ${WHITE} must return an error"
|
log error "test failed: ${WHITE} must return an error"
|
||||||
|
|||||||
Reference in New Issue
Block a user