Merge branch 'master' into next/vere

* master:
  [nix] track nixos-21.11 branch whenever niv updates, bump to include qemu-in-virtualization fix
  webterm: v1.0.0
  herm: permission checks
  herm: avoid trailing empty path segments
  pmnsh: update secp256k1 configure flags
  secp256k1: use nixpkgs provided secp256k1 and add to sources-pmnsh
  webterm: remove border, let term live in page
  theme: cleaning up a few mismatches
  webterm: handle old-style blits and belts
  build: correct lmdb static builds
  build: explicitly override h2o build platforms to support darwin
  ci: upgrade cachix/install-nix-action from v13 -> v16
  build: remove haskell related nix code and haskell.nix dependency
  webterm: update imports
  @urbit/api: move term types
  webterm: fix broken imports
  webterm: update package name for lerna
  webterm: commit missing api files
  webterm: backport
This commit is contained in:
Joe Bryan 2021-12-17 23:38:22 -05:00
commit 15148c72bf
43 changed files with 811 additions and 1360 deletions

View File

@ -39,7 +39,6 @@ on:
- 'pkg/docker-image/**'
- 'pkg/ent/**'
- 'pkg/ge-additions/**'
- 'pkg/hs/**'
- 'pkg/libaes_siv/**'
- 'pkg/urbit/**'
- 'bin/**'
@ -50,7 +49,6 @@ on:
- 'pkg/docker-image/**'
- 'pkg/ent/**'
- 'pkg/ge-additions/**'
- 'pkg/hs/**'
- 'pkg/libaes_siv/**'
- 'pkg/urbit/**'
- 'bin/**'
@ -74,12 +72,12 @@ jobs:
# for the docker build. We don't want in on Mac, where it isn't but
# it breaks the nix install. The two `if` clauses should be mutually
# exclusive
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v16
with:
extra_nix_config: |
system-features = nixos-test benchmark big-parallel kvm
if: ${{ matrix.os == 'ubuntu-latest' }}
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v16
if: ${{ matrix.os != 'ubuntu-latest' }}
- uses: cachix/cachix-action@v10
@ -95,28 +93,6 @@ jobs:
- if: ${{ matrix.os == 'ubuntu-latest' }}
run: nix-build -A docker-image
haskell:
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest }
- { os: macos-latest }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v13
- uses: cachix/cachix-action@v10
with:
name: ares
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- run: nix-build -A hs.urbit-king.components.exes.urbit-king --arg enableStatic true
- run: nix-build -A hs-checks
- run: nix-build shell.nix
mingw:
runs-on: windows-latest
defaults:

View File

@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v16
with:
extra_nix_config: |
system-features = nixos-test benchmark big-parallel kvm

View File

@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v16
- uses: cachix/cachix-action@v10
with:
name: ${{ secrets.CACHIX_NAME }}

View File

@ -14,10 +14,6 @@
$ nix-build -A urbit --argstr crossSystem x86_64-unknown-linux-musl \
--arg enableStatic true
Static urbit-king binary:
$ nix-build -A hs.urbit-king.components.exes.urbit-king --arg enableStatic true
Static release tarball:
$ nix-build -A tarball --arg enableStatic true
@ -28,15 +24,6 @@
$ nix-build -A brass.build
$ nix-build -A solid.build
Run the king-haskell checks (.tests are _build_ the test code, .checks _runs_):
$ nix-build -A hs.urbit-king.checks.urbit-king-tests
Build a specific Haskell package from ./pkg/hs:
$ nix-build -A hs.urbit-noun.components.library
$ nix-build -A hs.urbit-atom.components.benchmarks.urbit-atom-bench
$ nix-build -A hs.urbit-atom.components.tests.urbit-atom-tests
*/
# The build system where packages will be _built_.
@ -52,7 +39,7 @@
# Overlays to apply to the last package set in cross compilation.
, crossOverlays ? [ ]
# Whether to use pkgs.pkgsStatic.* to obtain statically linked package
# dependencies - ie. when building fully-static libraries or executables.
# dependencies - ie. when building fully-static libraries or executables.
, enableStatic ? false }:
let
@ -76,7 +63,7 @@ let
# Enrich the global package set with our local functions and packages.
# Cross vs static build dependencies can be selectively overridden for
# inputs like python and haskell-nix
# inputs like python etc.
callPackage =
pkgsNative.lib.callPackageWith (pkgsStatic // libLocal // pkgsLocal);
@ -113,22 +100,12 @@ let
urcrypt = callPackage ./nix/pkgs/urcrypt { inherit enableStatic; };
docker-image = callPackage ./nix/pkgs/docker-image { };
hs = callPackage ./nix/pkgs/hs {
inherit enableStatic;
inherit (pkgsCross) haskell-nix;
};
};
# Additional top-level packages and attributes exposed for convenience.
pkgsExtra = with pkgsLocal; rec {
# Expose packages with local customisations (like patches) for dev access.
inherit (pkgsCross) libsigsegv;
# Collect haskell check (aka "run the tests") attributes so we can run every
# test for our local haskell packages, similar to the urbit-tests attribute.
hs-checks = (pkgsNative.recurseIntoAttrs
(libLocal.collectHaskellComponents pkgsLocal.hs)).checks;
inherit (pkgsStatic) libsigsegv lmdb;
urbit-debug = urbit.override { enableDebug = true; };
urbit-tests = libLocal.testFakeShip {
@ -145,14 +122,12 @@ let
# Create a .tgz of the primary binaries.
tarball = let
name = "urbit-v${urbit.version}-${urbit.system}";
urbit-king = hs.urbit-king.components.exes.urbit-king;
in libLocal.makeReleaseTarball {
inherit name;
contents = {
"${name}/urbit" = "${urbit}/bin/urbit";
"${name}/urbit-worker" = "${urbit}/bin/urbit-worker";
"${name}/urbit-king" = "${urbit-king}/bin/urbit-king";
};
};

View File

@ -13,42 +13,19 @@
let
sourcesFinal = import ./sources.nix { inherit pkgs; } // sources;
finalSources = import ./sources.nix { } // sources;
haskellNix = import sourcesFinal."haskell.nix" {
sourcesOverride = {
hackage = sourcesFinal."hackage.nix";
stackage = sourcesFinal."stackage.nix";
};
};
pkgs = import finalSources.nixpkgs {
inherit system config crossSystem crossOverlays;
configFinal = haskellNix.config // config;
overlaysFinal = haskellNix.overlays ++ [
(_final: prev: {
# Add top-level .sources attribute for other overlays to access sources.
sources = sourcesFinal;
# Additional non-convential package/exe mappings for shellFor.tools.
haskell-nix = prev.haskell-nix // {
toolPackageName = prev.haskell-nix.toolPackageName // {
shellcheck = "ShellCheck";
};
};
})
# General unguarded (native) overrides for nixpkgs.
(import ./overlays/native.nix)
# Specific overrides guarded by the host platform.
(import ./overlays/musl.nix)
] ++ overlays;
pkgs = import sourcesFinal.nixpkgs {
inherit system crossSystem crossOverlays;
config = configFinal;
overlays = overlaysFinal;
overlays = [
# Make prev.sources available to subsequent overlays.
(_final: _prev: { sources = finalSources; })
# General unguarded (native) overrides for nixpkgs.
(import ./overlays/native.nix)
# Specific overrides guarded by the host platform.
(import ./overlays/musl.nix)
];
};
in pkgs // {

View File

@ -1,6 +1,6 @@
# Functions that are expected run on the native (non-cross) system.
{ lib, recurseIntoAttrs, haskell-nix, callPackage }:
{ callPackage }:
rec {
bootFakeShip = callPackage ./boot-fake-ship.nix { };
@ -10,28 +10,4 @@ rec {
fetchGitHubLFS = callPackage ./fetch-github-lfs.nix { };
makeReleaseTarball = callPackage ./make-release-tarball.nix { };
collectHaskellComponents = project:
let
# These functions pull out from the Haskell project either all the
# components of a particular type, or all the checks.
pkgs = haskell-nix.haskellLib.selectProjectPackages project;
collectChecks = _:
recurseIntoAttrs (builtins.mapAttrs (_: p: p.checks) pkgs);
collectComponents = type:
haskell-nix.haskellLib.collectComponents' type pkgs;
# Recompute the Haskell package set sliced by component type
in builtins.mapAttrs (type: f: f type) {
# These names must match the subcomponent: components.<name>.<...>
"library" = collectComponents;
"tests" = collectComponents;
"benchmarks" = collectComponents;
"exes" = collectComponents;
"checks" = collectChecks;
};
}

View File

@ -91,7 +91,7 @@ let
"''${curl[@]}" -s --output "$out" "$href"
'';
impureEnvVars = stdenvNoCC.lib.fetchers.proxyImpureEnvVars;
impureEnvVars = lib.fetchers.proxyImpureEnvVars;
SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";

View File

@ -23,6 +23,4 @@ in prev.lib.optionalAttrs isMusl {
rhash = overrideStdenv prev.rhash;
numactl = overrideStdenv prev.numactl;
lmdb = overrideStdenv prev.lmdb;
}

View File

@ -9,11 +9,7 @@ in {
version = final.sources.h2o.rev;
src = final.sources.h2o;
outputs = [ "out" "dev" "lib" ];
});
secp256k1 = prev.secp256k1.overrideAttrs (_attrs: {
version = final.sources.secp256k1.rev;
src = final.sources.secp256k1;
meta.platforms = prev.lib.platforms.linux ++ prev.lib.platforms.darwin;
});
libsigsegv = prev.libsigsegv.overrideAttrs (attrs: {
@ -23,7 +19,7 @@ in {
];
});
curlMinimal = prev.curl.override {
curlUrbit = prev.curlMinimal.override {
http2Support = false;
scpSupport = false;
gssSupport = false;

View File

@ -16,7 +16,7 @@ let
in {
gmp = enableStatic prev.gmp;
curlMinimal = enableStatic prev.curlMinimal;
curlUrbit = enableStatic prev.curlUrbit;
libuv = enableStatic prev.libuv;
@ -26,12 +26,8 @@ in {
lmdb = prev.lmdb.overrideAttrs (old:
configureFlags old // {
# Why remove the so version? It's easier than preventing it from being
# built with lmdb's custom Makefiles, and it can't exist in the output
# because otherwise the linker will preferentially choose the .so over
# the .a.
postInstall = ''
rm $out/lib/liblmdb.so
postPatch = ''
sed '/^ILIBS\t/s/liblmdb\$(SOEXT)//' -i Makefile
'';
});
}

View File

@ -18,7 +18,7 @@ stdenvNoCC.mkDerivation {
phases = [ "installPhase" "fixupPhase" ];
installPhase = ''
mkdir -p $out/bin
mkdir -p $out/bin
cp $src $out/bin/herb
chmod +x $out/bin/herb
'';

View File

@ -1,90 +0,0 @@
{ lib, stdenv, darwin, haskell-nix, lmdb, gmp, zlib, libffi, brass
, enableStatic ? stdenv.hostPlatform.isStatic }:
haskell-nix.stackProject {
compiler-nix-name = "ghc884";
index-state = "2020-09-24T00:00:00Z";
# This is incredibly difficult to get right, almost everything goes wrong.
# See: https://github.com/input-output-hk/haskell.nix/issues/496
src = haskell-nix.haskellLib.cleanSourceWith {
# Otherwise this depends on the name in the parent directory, which
# reduces caching, and is particularly bad on Hercules.
# See: https://github.com/hercules-ci/support/issues/40
name = "urbit-hs";
src = ../../../pkg/hs;
};
modules = [{
# This corresponds to the set of packages (boot libs) that ship with GHC.
# We declare them yere to ensure any dependency gets them from GHC itself
# rather than trying to re-install them into the package database.
nonReinstallablePkgs = [
"Cabal"
"Win32"
"array"
"base"
"binary"
"bytestring"
"containers"
"deepseq"
"directory"
"filepath"
"ghc"
"ghc-boot"
"ghc-boot-th"
"ghc-compact"
"ghc-heap"
"ghc-prim"
"ghci"
"ghcjs-prim"
"ghcjs-th"
"haskeline"
"hpc"
"integer-gmp"
"integer-simple"
"mtl"
"parsec"
"pretty"
"process"
"rts"
"stm"
"template-haskell"
"terminfo"
"text"
"time"
"transformers"
"unix"
"xhtml"
];
# Override various project-local flags and build configuration.
packages = {
urbit-king.components.exes.urbit-king = {
enableStatic = enableStatic;
enableShared = !enableStatic;
configureFlags = lib.optionals enableStatic [
"--ghc-option=-optl=-L${lmdb}/lib"
"--ghc-option=-optl=-L${gmp}/lib"
"--ghc-option=-optl=-L${libffi}/lib"
"--ghc-option=-optl=-L${zlib}/lib"
] ++ lib.optionals (enableStatic && stdenv.isDarwin)
[ "--ghc-option=-optl=-L${darwin.libiconv}/lib" ];
postInstall = lib.optionalString (enableStatic && stdenv.isDarwin) ''
find "$out/bin" -type f -exec \
install_name_tool -change \
${stdenv.cc.libc}/lib/libSystem.B.dylib \
/usr/lib/libSystem.B.dylib {} \;
'';
};
urbit-king.components.tests.urbit-king-tests.testFlags =
[ "--brass-pill=${brass.lfs}" ];
lmdb.components.library.libs = lib.mkForce [ lmdb ];
};
}];
}

View File

@ -1,6 +1,6 @@
{ lib, stdenv, coreutils, pkgconfig # build/env
, cacert, ca-bundle, ivory # codegen
, curlMinimal, ent, gmp, h2o, libsigsegv, libuv, lmdb # libs
, curlUrbit, ent, gmp, h2o, libsigsegv, libuv, lmdb # libs
, murmur3, openssl, softfloat3, urcrypt, zlib #
, enableStatic ? stdenv.hostPlatform.isStatic # opts
, enableDebug ? false
@ -25,7 +25,7 @@ in stdenv.mkDerivation {
buildInputs = [
cacert
ca-bundle
curlMinimal
curlUrbit
ent
gmp
h2o

View File

@ -31,6 +31,24 @@
"url": "https://github.com/LMDB/lmdb/archive/48a7fed59a8aae623deff415dda27097198ca0c1.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"secp256k1": {
"branch": "master",
"description": "Optimized C library for ECDSA signatures and secret/public key operations on curve secp256k1.",
"homepage": null,
"owner": "bitcoin-core",
"pmnsh": {
"include": "include",
"lib": ".libs",
"make": "libsecp256k1.la",
"prepare": "./autogen.sh && ./configure --disable-shared --enable-benchmark=no --enable-exhaustive-tests=no --enable-experimental --enable-module-ecdh --enable-module-recovery --enable-module-schnorrsig --enable-tests=yes CFLAGS=-DSECP256K1_API="
},
"repo": "secp256k1",
"rev": "7973576f6e3ab27d036a09397152b124d747f4ae",
"sha256": "0vjk55dv0mkph4k6bqgkykmxn05ngzvhc4rzjnvn33xzi8dzlvah",
"type": "tarball",
"url": "https://github.com/bitcoin-core/secp256k1/archive/7973576f6e3ab27d036a09397152b124d747f4ae.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"uv": {
"branch": "v1.x",
"description": "Cross-platform asynchronous I/O",

View File

@ -3,17 +3,17 @@
"branch": "master",
"description": "H2O - the optimized HTTP/1, HTTP/2, HTTP/3 server",
"homepage": "https://h2o.examp1e.net",
"pmnsh": {
"include": "include",
"prepare": "cmake .",
"make": "libh2o",
"compat": {
"mingw": {
"prepare": "cmake -G\"MSYS Makefiles\" -DCMAKE_INSTALL_PREFIX=. ."
}
}
},
"owner": "h2o",
"pmnsh": {
"compat": {
"mingw": {
"prepare": "cmake -G\"MSYS Makefiles\" -DCMAKE_INSTALL_PREFIX=. ."
}
},
"include": "include",
"make": "libh2o",
"prepare": "cmake ."
},
"repo": "h2o",
"rev": "v2.2.6",
"sha256": "0qni676wqvxx0sl0pw9j0ph7zf2krrzqc1zwj73mgpdnsr8rsib7",
@ -21,47 +21,23 @@
"url": "https://github.com/h2o/h2o/archive/v2.2.6.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"hackage.nix": {
"branch": "master",
"description": "Automatically generated Nix expressions for Hackage",
"homepage": "",
"owner": "input-output-hk",
"repo": "hackage.nix",
"rev": "ed4d2759c9e6ca8133a4170f99fabdd76f30f51a",
"sha256": "1n5fk8zsxnbca96zk4ikh74iz3lzh35m302q65zk1rx3nmy4027d",
"type": "tarball",
"url": "https://github.com/input-output-hk/hackage.nix/archive/ed4d2759c9e6ca8133a4170f99fabdd76f30f51a.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"haskell.nix": {
"branch": "master",
"description": "Alternative Haskell Infrastructure for Nixpkgs",
"homepage": "https://input-output-hk.github.io/haskell.nix",
"owner": "input-output-hk",
"repo": "haskell.nix",
"rev": "bbb34dcdf7b90d478002f91713531f418ddf1b53",
"sha256": "1qq397j8vnlp5npk8r675fzjfimg74fcvrkxcdgx7vj48315bh2w",
"type": "tarball",
"url": "https://github.com/input-output-hk/haskell.nix/archive/bbb34dcdf7b90d478002f91713531f418ddf1b53.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"libaes_siv": {
"branch": "master",
"description": null,
"homepage": null,
"owner": "dfoxfranke",
"pmnsh": {
"compat": {
"m1brew": {
"prepare": "cmake .",
"make": "install CFLAGS=$(pkg-config --cflags openssl)"
"make": "install CFLAGS=$(pkg-config --cflags openssl)",
"prepare": "cmake ."
},
"mingw": {
"prepare": "cmake -G\"MSYS Makefiles\" -DDISABLE_DOCS:BOOL=ON .",
"make": "aes_siv_static"
"make": "aes_siv_static",
"prepare": "cmake -G\"MSYS Makefiles\" -DDISABLE_DOCS:BOOL=ON ."
}
}
},
"owner":"dfoxfranke",
"repo": "libaes_siv",
"rev": "9681279cfaa6e6399bb7ca3afbbc27fc2e19df4b",
"sha256": "1g4wy0m5wpqx7z6nillppkh5zki9fkx9rdw149qcxh7mc5vlszzi",
@ -73,10 +49,10 @@
"branch": "master",
"description": null,
"homepage": null,
"owner": "urbit",
"pmnsh": {
"make": "static"
},
"owner": "urbit",
"repo": "murmur3",
"rev": "71a75d57ca4e7ca0f7fc2fd84abd93595b0624ca",
"sha256": "0k7jq2nb4ad9ajkr6wc4w2yy2f2hkwm3nkbj2pklqgwsg6flxzwg",
@ -97,23 +73,23 @@
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixpkgs": {
"branch": "master",
"branch": "nixos-21.11",
"description": "Nix Packages collection",
"homepage": null,
"owner": "nixos",
"homepage": "",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "166ab9d237409c4b74b1f8ca31476ead35e8fe53",
"sha256": "13i43kvbkdl3dh8b986j6mxbn355mqjhcxrd8cni8zfx1z0wrscr",
"rev": "573095944e7c1d58d30fc679c81af63668b54056",
"sha256": "07s5cwhskqvy82b4rld9b14ljc0013pig23i3jx3l3f957rk95pg",
"type": "tarball",
"url": "https://github.com/nixos/nixpkgs/archive/166ab9d237409c4b74b1f8ca31476ead35e8fe53.tar.gz",
"url": "https://github.com/NixOS/nixpkgs/archive/573095944e7c1d58d30fc679c81af63668b54056.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"softfloat3": {
"branch": "master",
"description": null,
"homepage": null,
"owner": "urbit",
"pmnsh": {
"include": "source/include",
"compat": {
"m1brew": {
"lib": "build/template-FAST_INT64",
@ -123,44 +99,14 @@
"lib": "build/Win64-MinGW-w64",
"make": "-C build/Win64-MinGW-w64 libsoftfloat3.a"
}
}
},
"include": "source/include"
},
"owner": "urbit",
"repo": "berkeley-softfloat-3",
"rev": "ec4c7e31b32e07aad80e52f65ff46ac6d6aad986",
"sha256": "1lz4bazbf7lns1xh8aam19c814a4n4czq5xsq5rmi9sgqw910339",
"type": "tarball",
"url": "https://github.com/urbit/berkeley-softfloat-3/archive/ec4c7e31b32e07aad80e52f65ff46ac6d6aad986.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"secp256k1": {
"branch": "master",
"description": "Optimized C library for ECDSA signatures and secret/public key operations on curve secp256k1.",
"homepage": null,
"pmnsh": {
"include": "include",
"lib": ".libs",
"prepare": "./autogen.sh && ./configure --disable-shared --enable-module-recovery CFLAGS=-DSECP256K1_API=",
"make": "libsecp256k1.la"
},
"owner": "bitcoin-core",
"repo": "secp256k1",
"rev": "26de4dfeb1f1436dae1fcf17f57bdaa43540f940",
"sha256": "03i3nv8d3ci7q9y98q11rrp3rvwdqc0hc0ss0pr6xckybvizsmbb",
"type": "tarball",
"url": "https://github.com/bitcoin-core/secp256k1/archive/26de4dfeb1f1436dae1fcf17f57bdaa43540f940.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"stackage.nix": {
"branch": "master",
"description": "Automatically generated Nix expressions of Stackage snapshots",
"homepage": "",
"owner": "input-output-hk",
"repo": "stackage.nix",
"rev": "08312f475f4f5f3b6578e7a78dc501de6fea8792",
"sha256": "15j1l6616kfv7351jxwgb9kj6y8227fcm87nxwabmbn1q6a8q2kf",
"type": "tarball",
"url": "https://github.com/input-output-hk/stackage.nix/archive/08312f475f4f5f3b6578e7a78dc501de6fea8792.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}

View File

@ -6,149 +6,169 @@ let
# The fetchers. fetch_<type> fetches specs of type <type>.
#
fetch_file = pkgs: spec:
if spec.builtin or true then
builtins_fetchurl { inherit (spec) url sha256; }
else
pkgs.fetchurl { inherit (spec) url sha256; };
fetch_file = pkgs: name: spec:
let
name' = sanitizeName name + "-src";
in
if spec.builtin or true then
builtins_fetchurl { inherit (spec) url sha256; name = name'; }
else
pkgs.fetchurl { inherit (spec) url sha256; name = name'; };
fetch_tarball = pkgs: name: spec:
let
ok = str: !builtins.isNull (builtins.match "[a-zA-Z0-9+-._?=]" str);
# sanitize the name, though nix will still fail if name starts with period
name' = stringAsChars (x: if !ok x then "-" else x) "${name}-src";
in if spec.builtin or true then
builtins_fetchTarball {
name = name';
inherit (spec) url sha256;
}
else
pkgs.fetchzip {
name = name';
inherit (spec) url sha256;
};
name' = sanitizeName name + "-src";
in
if spec.builtin or true then
builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
else
pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
fetch_git = spec:
builtins.fetchGit {
url = spec.repo;
inherit (spec) rev ref;
};
fetch_git = name: spec:
let
ref =
if spec ? ref then spec.ref else
if spec ? branch then "refs/heads/${spec.branch}" else
if spec ? tag then "refs/tags/${spec.tag}" else
abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
in
builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; };
fetch_local = spec: spec.path;
fetch_builtin-tarball = name:
throw ''
[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=tarball -a builtin=true'';
fetch_builtin-tarball = name: throw
''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=tarball -a builtin=true'';
fetch_builtin-url = name:
throw ''
[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=file -a builtin=true'';
fetch_builtin-url = name: throw
''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=file -a builtin=true'';
#
# Various helpers
#
# https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
sanitizeName = name:
(
concatMapStrings (s: if builtins.isList s then "-" else s)
(
builtins.split "[^[:alnum:]+._?=-]+"
((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
)
);
# The set of packages used when specs are fetched using non-builtins.
mkPkgs = sources:
mkPkgs = sources: system:
let
sourcesNixpkgs =
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; })
{ };
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
in if builtins.hasAttr "nixpkgs" sources then
sourcesNixpkgs
else if hasNixpkgsPath && !hasThisAsNixpkgsPath then
import <nixpkgs> { }
else
abort ''
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
add a package called "nixpkgs" to your sources.json.
'';
in
if builtins.hasAttr "nixpkgs" sources
then sourcesNixpkgs
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
import <nixpkgs> {}
else
abort
''
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
add a package called "nixpkgs" to your sources.json.
'';
# The actual fetching function.
fetch = pkgs: name: spec:
if !builtins.hasAttr "type" spec then
if ! builtins.hasAttr "type" spec then
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
else if spec.type == "file" then
fetch_file pkgs spec
else if spec.type == "tarball" then
fetch_tarball pkgs name spec
else if spec.type == "git" then
fetch_git spec
else if spec.type == "local" then
fetch_local spec
else if spec.type == "builtin-tarball" then
fetch_builtin-tarball name
else if spec.type == "builtin-url" then
fetch_builtin-url name
else if spec.type == "file" then fetch_file pkgs name spec
else if spec.type == "tarball" then fetch_tarball pkgs name spec
else if spec.type == "git" then fetch_git name spec
else if spec.type == "local" then fetch_local spec
else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
else if spec.type == "builtin-url" then fetch_builtin-url name
else
abort
"ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
# If the environment variable NIV_OVERRIDE_${name} is set, then use
# the path directly as opposed to the fetched source.
replace = name: drv:
let
saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name;
ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
in
if ersatz == "" then drv else
# this turns the string into an actual Nix path (for both absolute and
# relative paths)
if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}";
# Ports of functions for older nix versions
# a Nix version of mapAttrs if the built-in doesn't exist
mapAttrs = builtins.mapAttrs or (f: set:
with builtins;
listToAttrs (map (attr: {
name = attr;
value = f attr set.${attr};
}) (attrNames set)));
mapAttrs = builtins.mapAttrs or (
f: set: with builtins;
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
);
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
range = first: last:
if first > last then
[ ]
else
builtins.genList (n: first + n) (last - first + 1);
range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1);
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
stringToCharacters = s:
map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
concatMapStrings = f: list: concatStrings (map f list);
concatStrings = builtins.concatStringsSep "";
# https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
optionalAttrs = cond: as: if cond then as else {};
# fetchTarball version that is compatible between all the versions of Nix
builtins_fetchTarball = { url, name, sha256 }@attrs:
let inherit (builtins) lessThan nixVersion fetchTarball;
in if lessThan nixVersion "1.12" then
fetchTarball { inherit name url; }
else
fetchTarball attrs;
builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchTarball;
in
if lessThan nixVersion "1.12" then
fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
else
fetchTarball attrs;
# fetchurl version that is compatible between all the versions of Nix
builtins_fetchurl = { url, sha256 }@attrs:
let inherit (builtins) lessThan nixVersion fetchurl;
in if lessThan nixVersion "1.12" then
fetchurl { inherit url; }
else
fetchurl attrs;
builtins_fetchurl = { url, name ? null, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchurl;
in
if lessThan nixVersion "1.12" then
fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
else
fetchurl attrs;
# Create the final "sources" from the config
mkSources = config:
mapAttrs (name: spec:
if builtins.hasAttr "outPath" spec then
abort
"The values in sources.json should not have an 'outPath' attribute"
else
spec // { outPath = fetch config.pkgs name spec; }) config.sources;
mapAttrs (
name: spec:
if builtins.hasAttr "outPath" spec
then abort
"The values in sources.json should not have an 'outPath' attribute"
else
spec // { outPath = replace name (fetch config.pkgs name spec); }
) config.sources;
# The "config" used by the fetchers
mkConfig = { sourcesFile ? ./sources.json
, sources ? builtins.fromJSON (builtins.readFile sourcesFile)
, pkgs ? mkPkgs sources }: rec {
mkConfig =
{ sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
, sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile)
, system ? builtins.currentSystem
, pkgs ? mkPkgs sources system
}: rec {
# The sources, i.e. the attribute set of spec name to spec
inherit sources;
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
inherit pkgs;
};
in mkSources (mkConfig { }) // {
__functor = _: settings: mkSources (mkConfig settings);
}
in
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }

View File

@ -32,7 +32,8 @@
++ on-watch
|= =path
^- (quip card:agent:gall _this)
?> ?=([%session @ ~] path)
?> =(our src):bowl
?> ?=([%session @ %view ~] path)
:_ this
:: scry prompt and cursor position out of dill for initial response
::
@ -57,12 +58,13 @@
:_ this
%+ turn p.sign-arvo
|= =blit:dill
[%give %fact [%session %$ ~]~ %blit !>(blit)]
[%give %fact [%session %$ %view ~]~ %blit !>(blit)]
==
::
++ on-poke
|= [=mark =vase]
^- (quip card:agent:gall _this)
?> =(our src):bowl
?. ?=(%belt mark)
~| [%unexpected-mark mark]
!!

View File

@ -1 +0,0 @@
import ../../shell.nix

View File

@ -0,0 +1,8 @@
import Urbit from '@urbit/http-api';
const api = new Urbit('', '', (window as any).desk);
api.ship = window.ship;
// api.verbose = true;
// @ts-ignore TODO window typings
window.api = api;
export default api;

View File

@ -1,50 +0,0 @@
import _ from 'lodash';
export default class Api {
ship: any;
channel: any;
bindPaths: any[];
constructor(ship, channel) {
this.ship = ship;
this.channel = channel;
this.bindPaths = [];
}
bind(path, method, ship = this.ship, appl = 'herm', success, fail) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
(window as any).subscriptionId = this.channel.subscribe(ship, appl, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(err) => {
fail(err);
});
}
belt(belt) {
return this.action('herm', 'belt', belt);
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
this.channel.poke(window.ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
}

View File

@ -1,94 +1,471 @@
import { Box, Col } from '@tlon/indigo-react';
import React, { Component } from 'react';
import dark from '@tlon/indigo-dark';
import light from '@tlon/indigo-light';
/* eslint-disable max-lines */
import React, {
useEffect,
useRef,
useCallback
} from 'react';
import useTermState from './state';
import { useDark } from './join';
import api from './api';
import { Terminal, ITerminalOptions, ITheme } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { saveAs } from 'file-saver';
import { Box, Col, Reset, _dark, _light } from '@tlon/indigo-react';
import 'xterm/css/xterm.css';
import {
Belt, Blit, Stye, Stub, Tint, Deco,
pokeTask, pokeBelt
} from '@urbit/api/term';
import bel from './lib/bel';
import { ThemeProvider } from 'styled-components';
import Api from './api';
import { History } from './components/history';
import { Input } from './components/input';
import './css/custom.css';
import Store from './store';
import Subscription from './subscription';
import Channel from './lib/channel';
class TermApp extends Component<any, any> {
store: Store;
api: any;
subscription: any;
constructor(props) {
super(props);
this.store = new Store();
this.store.setStateHandler(this.setState.bind(this));
type TermAppProps = {
ship: string;
}
this.state = this.store.state;
const makeTheme = (dark: boolean): ITheme => {
let fg, bg: string;
if (dark) {
fg = 'white';
bg = 'rgb(26,26,26)';
} else {
fg = 'black';
bg = 'white';
}
// TODO indigo colors.
// we can't pluck these from ThemeContext because they have transparency.
// technically xterm supports transparency, but it degrades performance.
return {
foreground: fg,
background: bg,
brightBlack: '#7f7f7f', // NOTE slogs
cursor: fg
};
};
const termConfig: ITerminalOptions = {
logLevel: 'warn',
//
convertEol: true,
//
rows: 24,
cols: 80,
scrollback: 10000,
//
fontFamily: '"Source Code Pro", "Roboto mono", "Courier New", monospace',
fontWeight: 400,
// NOTE theme colors configured dynamically
//
bellStyle: 'sound',
bellSound: bel,
//
// allows text selection by holding modifier (option, or shift)
macOptionClickForcesSelection: true
};
const csi = (cmd: string, ...args: number[]) => {
return '\x1b[' + args.join(';') + cmd;
};
const tint = (t: Tint) => {
switch (t) {
case null: return '9';
case 'k': return '0';
case 'r': return '1';
case 'g': return '2';
case 'y': return '3';
case 'b': return '4';
case 'm': return '5';
case 'c': return '6';
case 'w': return '7';
default: return `8;2;${t.r%256};${t.g%256};${t.b%256}`;
}
};
const stye = (s: Stye) => {
let out = '';
// text decorations
//
if (s.deco.length > 0) {
out += s.deco.reduce((decs: number[], deco: Deco) => {
/* eslint-disable max-statements-per-line */
switch (deco) {
case null: decs.push(0); return decs;
case 'br': decs.push(1); return decs;
case 'un': decs.push(4); return decs;
case 'bl': decs.push(5); return decs;
default: console.log('weird deco', deco); return decs;
}
}, []).join(';');
}
resetControllers() {
this.api = null;
this.subscription = null;
// background color
//
if (s.back !== null) {
if (out !== '') {
out += ';';
}
out += '4';
out += tint(s.back);
}
componentDidMount() {
this.resetControllers();
// eslint-disable-next-line new-cap
const channel = new Channel();
this.api = new Api(window.ship, channel);
this.store.api = this.api;
this.subscription = new Subscription(this.store, this.api, channel);
this.subscription.start();
// foreground color
//
if (s.fore !== null) {
if (out !== '') {
out += ';';
}
out += '3';
out += tint(s.fore);
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.resetControllers();
if (out === '') {
return out;
}
return '\x1b[' + out + 'm';
};
const showBlit = (term: Terminal, blit: Blit) => {
let out = '';
if ('bel' in blit) {
out += '\x07';
} else if ('clr' in blit) {
term.clear();
out += csi('u');
} else if ('hop' in blit) {
if (typeof blit.hop === 'number') {
out += csi('H', term.rows, blit.hop + 1);
} else {
out += csi('H', term.rows - blit.hop.r, blit.hop.c + 1);
}
out += csi('s'); // save cursor position
} else if ('put' in blit) {
out += blit.put.join('');
out += csi('u');
} else if ('klr' in blit) {
//TODO remove for new backend
{
out += csi('H', term.rows, 1);
out += csi('K');
}
out += blit.klr.reduce((lin: string, p: Stub) => {
lin += stye(p.stye);
lin += p.text.join('');
lin += csi('m', 0);
return lin;
}, '');
out += csi('u');
} else if ('nel' in blit) {
out += '\n';
} else if ('sag' in blit || 'sav' in blit) {
const sav = ('sag' in blit) ? blit.sag : blit.sav;
const name = sav.path.split('/').slice(-2).join('.');
const buff = Buffer.from(sav.file, 'base64');
const blob = new Blob([buff], { type: 'application/octet-stream' });
saveAs(blob, name);
} else if ('url' in blit) {
window.open(blit.url);
} else if ('wyp' in blit) {
out += '\r' + csi('K');
out += csi('u');
//
//TODO remove for new backend
} else if ('lin' in blit) {
out += csi('H', term.rows, 1);
out += csi('K');
out += blit.lin.join('');
} else if ('mor' in blit) {
out += '\n';
} else {
console.log('weird blit', blit);
}
getTheme() {
const { props } = this;
return ((props.dark && props?.display?.theme == 'auto') ||
props?.display?.theme == 'dark'
) ? dark : light;
}
term.write(out);
};
render() {
const theme = this.getTheme();
return (
<ThemeProvider theme={theme}>
// NOTE should generally only be passed the default terminal session
const showSlog = (term: Terminal, slog: string) => {
// set scroll region to exclude the bottom line,
// scroll up one line,
// move cursor to start of the newly created whitespace,
// set text to grey,
// print the slog,
// restore color, scroll region, and cursor.
//
term.write(csi('r', 1, term.rows - 1)
+ csi('S', 1)
+ csi('H', term.rows - 1, 1)
+ csi('m', 90)
+ slog
+ csi('m', 0)
+ csi('r')
+ csi('u'));
};
const readInput = (term: Terminal, e: string): Belt[] => {
const belts: Belt[] = [];
let strap = '';
while (e.length > 0) {
let c = e.charCodeAt(0);
// text input
//
if (c >= 32 && c !== 127) {
strap += e[0];
e = e.slice(1);
continue;
} else if ('' !== strap) {
belts.push({ txt: strap.split('') });
strap = '';
}
// special keys/characters
//
if (0 === c) {
term.write('\x07'); // bel
} else if (8 === c || 127 === c) {
belts.push({ bac: null });
} else if (13 === c) {
belts.push({ ret: null });
} else if (c <= 26) {
let k = String.fromCharCode(96 + c);
//NOTE prevent remote shut-downs
if ('d' !== k) {
belts.push({ ctl: k });
//TODO for new backend
// belts.push({ mod: { mod: 'ctl', key: k } });
}
}
// escape sequences
//
if (27 === c) { // ESC
e = e.slice(1);
c = e.charCodeAt(0);
if (91 === c || 79 === c) { // [ or O
e = e.slice(1);
c = e.charCodeAt(0);
/* eslint-disable max-statements-per-line */
switch (c) {
case 65: belts.push({ aro: 'u' }); break;
case 66: belts.push({ aro: 'd' }); break;
case 67: belts.push({ aro: 'r' }); break;
case 68: belts.push({ aro: 'l' }); break;
//
case 77: {
const m = e.charCodeAt(1) - 31;
if (1 === m) {
const c = e.charCodeAt(2) - 32;
const r = e.charCodeAt(3) - 32;
//TODO re-enable for new backend
// belts.push({ hit: { r: term.rows - r, c: c - 1 } });
}
e = e.slice(3);
break;
}
//
default: term.write('\x07'); break; // bel
}
} else if (c >= 97 && c <= 122) { // a <= c <= z
belts.push({ mod: { mod: 'met', key: e[0] } });
} else if (c === 46) { // .
belts.push({ mod: { mod: 'met', key: '.' } });
} else if (c === 8 || c === 127) {
belts.push({ mod: { mod: 'met', key: { bac: null } } });
} else {
term.write('\x07'); break; // bel
}
}
e = e.slice(1);
}
if ('' !== strap) {
belts.push({ txt: strap.split('') });
strap = '';
}
return belts;
};
export default function TermApp(props: TermAppProps) {
const container = useRef<HTMLDivElement>(null);
// TODO allow switching of selected
const { sessions, selected, slogstream, set } = useTermState();
const session = sessions[selected];
const dark = useDark();
const setupSlog = useCallback(() => {
console.log('slog: setting up...');
let available = false;
const slog = new EventSource('/~_~/slog', { withCredentials: true });
slog.onopen = (e) => {
console.log('slog: opened stream');
available = true;
};
slog.onmessage = (e) => {
const session = useTermState.getState().sessions[''];
if (!session) {
console.log('default session mia!', 'slog:', slog);
return;
}
showSlog(session.term, e.data);
};
slog.onerror = (e) => {
console.error('slog: eventsource error:', e);
if (available) {
window.setTimeout(() => {
if (slog.readyState !== EventSource.CLOSED) {
return;
}
console.log('slog: reconnecting...');
setupSlog();
}, 10000);
}
};
set((state) => {
state.slogstream = slog;
});
}, [sessions]);
const onInput = useCallback((ses: string, e: string) => {
const term = useTermState.getState().sessions[ses].term;
const belts = readInput(term, e);
belts.map((b) => { // NOTE passing api.poke(pokeBelt makes `this` undefined!
//TODO pokeBelt(ses, b);
api.poke({
app: 'herm',
mark: 'belt',
json: b
});
});
}, [sessions]);
const onResize = useCallback(() => {
// TODO debounce, if it ever becomes a problem
session?.fit.fit();
}, [session]);
// on-init, open slogstream
//
useEffect(() => {
if (!slogstream) {
setupSlog();
}
window.addEventListener('resize', onResize);
return () => {
// TODO clean up subs?
window.removeEventListener('resize', onResize);
};
}, [onResize, setupSlog]);
// on dark mode change, change terminals' theme
//
useEffect(() => {
const theme = makeTheme(dark);
for (const ses in sessions) {
sessions[ses].term.setOption('theme', theme);
}
if (container.current) {
container.current.style.backgroundColor = theme.background || '';
}
}, [dark, sessions]);
// on selected change, maybe setup the term, or put it into the container
//
useEffect(() => {
let ses = session;
// initialize terminal
//
if (!ses) {
// set up terminal
//
const term = new Terminal(termConfig);
term.setOption('theme', makeTheme(dark));
const fit = new FitAddon();
term.loadAddon(fit);
// start mouse reporting
//
term.write(csi('?9h'));
// set up event handlers
//
term.onData(e => onInput(selected, e));
term.onBinary(e => onInput(selected, e));
term.onResize((e) => {
//TODO re-enable once new backend lands
// api.poke(pokeTask(selected, { blew: { w: e.cols, h: e.rows } }));
});
ses = { term, fit };
// open subscription
//
api.subscribe({ app: 'herm', path: '/session/'+selected+'/view',
event: (e) => {
const ses = useTermState.getState().sessions[selected];
if (!ses) {
console.log('on blit: no such session', selected, sessions, useTermState.getState().sessions);
return;
}
showBlit(ses.term, e);
},
quit: () => { // quit
// TODO show user a message
}
});
}
if (container.current && !container.current.contains(ses.term.element || null)) {
ses.term.open(container.current);
ses.fit.fit();
ses.term.focus();
}
set((state) => {
state.sessions[selected] = ses;
});
return () => {
// TODO unload term from container
// but term.dispose is too powerful? maybe just empty the container?
};
}, [set, session, container]);
return (
<>
<ThemeProvider theme={dark ? _dark : _light}>
<Reset />
<Box
width='100%'
height='100%'
p={['0','3']}
style={{ boxSizing: 'border-box' }}
bg='white'
fontFamily='mono'
overflow='hidden'
>
<Col
p={3}
backgroundColor='white'
width='100%'
height='100%'
minHeight={0}
minWidth={0}
color='lightGray'
borderRadius={2}
border={['0','1']}
cursor='text'
style={{ boxSizing: 'border-box' }}
minHeight='0'
px={['0','2']}
pb={['0','2']}
ref={container}
>
{/* @ts-ignore declare props in later pass */}
<History log={this.state.lines.slice(0, -1)} />
<Input
ship={this.props.ship}
cursor={this.state.cursor}
api={this.api}
store={this.store}
line={this.state.lines.slice(-1)[0]}
/>
</Col>
</Box>
</ThemeProvider>
);
}
</>
);
}
export default TermApp;

View File

@ -1,35 +0,0 @@
import { Box } from '@tlon/indigo-react';
import React, { Component } from 'react';
import Line from './line';
export class History extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Box
height='100%'
minHeight={0}
minWidth={0}
display='flex'
flexDirection='column-reverse'
overflowY='scroll'
style={{ resize: 'none' }}
>
<Box
mt='auto'
>
{/* @ts-ignore declare props in later pass */}
{this.props.log.map((line, i) => {
// @ts-ignore react memo not passing props
return <Line key={i} line={line} />;
})}
</Box>
</Box>
);
}
}
export default History;

View File

@ -1,128 +0,0 @@
import { BaseInput, Box, Row } from '@tlon/indigo-react';
import React, { Component } from 'react';
export class Input extends Component<any, {}> {
inputRef: React.RefObject<unknown>;
constructor(props) {
super(props);
this.state = {};
this.keyPress = this.keyPress.bind(this);
this.paste = this.paste.bind(this);
this.click = this.click.bind(this);
this.inputRef = React.createRef();
}
componentDidUpdate() {
if (
document.activeElement == this.inputRef.current
) {
// @ts-ignore ref type issues
this.inputRef.current.focus();
// @ts-ignore ref type issues
this.inputRef.current.setSelectionRange(this.props.cursor, this.props.cursor);
}
}
keyPress(e) {
const key = e.key;
// let paste and leap events pass
if ((e.getModifierState('Control') || e.getModifierState('Meta'))
&& (e.key === 'v' || e.key === '/')) {
return;
}
let belt = null;
if (key === 'ArrowLeft')
belt = { aro: 'l' };
else if (key === 'ArrowRight')
belt = { aro: 'r' };
else if (key === 'ArrowUp')
belt = { aro: 'u' };
else if (key === 'ArrowDown')
belt = { aro: 'd' };
else if (key === 'Backspace')
belt = { bac: null };
else if (key === 'Delete')
belt = { del: null };
else if (key === 'Tab')
belt = { ctl: 'i' };
else if (key === 'Enter')
belt = { ret: null };
else if (key.length === 1)
belt = { txt: [key] };
else
belt = null;
if (belt && e.getModifierState('Control')) {
if (belt.txt !== undefined)
belt = { ctl: belt.txt[0] };
} else
if (belt &&
(e.getModifierState('Meta') || e.getModifierState('Alt'))) {
if (belt.bac !== undefined)
belt = { met: 'bac' };
}
if (belt !== null) {
this.props.api.belt(belt);
}
e.preventDefault();
}
paste(e) {
const clipboardData = e.clipboardData || (window as any).clipboardData;
const clipboardText = clipboardData.getData('Text');
this.props.api.belt({ txt: [...clipboardText] });
e.preventDefault();
}
click(e) {
// prevent desynced cursor movement
e.preventDefault();
e.target.setSelectionRange(this.props.cursor, this.props.cursor);
}
render() {
const line = this.props.line;
let prompt = 'connecting...';
if (line) {
if (line.lin) {
prompt = line.lin.join('');
} else if (line.klr) {
// TODO render prompt style
prompt = line.klr.reduce((l, p) => (l + p.text.join('')), '');
}
}
return (
<Row flexGrow={1} position='relative'>
<Box flexShrink={0} width='100%' color='black' fontSize={0}>
<BaseInput
autoFocus
autoCorrect="off"
autoCapitalize="off"
color='lightGray'
minHeight={0}
display='inline-block'
width='100%'
spellCheck="false"
tabindex={0}
wrap="off"
fontFamily="mono"
id="term"
cursor={this.props.cursor}
onKeyDown={this.keyPress}
onClick={this.click}
onPaste={this.paste}
// @ts-ignore indigo-react doesn't let us pass refs
ref={this.inputRef}
defaultValue="connecting..."
value={prompt}
/>
</Box>
</Row>
);
}
}
export default Input;

View File

@ -1,66 +0,0 @@
import { Text } from '@tlon/indigo-react';
import React from 'react';
// @ts-ignore line isn't in props?
export default React.memo(({ line }) => {
// line body to jsx
// NOTE lines are lists of characters that might span multiple codepoints
//
let text = '';
if (line.lin) {
text = line.lin.join('');
} else if (line.klr) {
text = line.klr.map((part, i) => {
const prop = part.stye.deco.reduce((prop, deco) => {
switch (deco) {
case null: return prop;
case 'br': return { bold: true, ...prop };
case 'bl': return { className: 'blink', ...prop };
case 'un': return { style: { textDecoration: 'underline' }, ...prop };
default: console.log('weird deco', deco); return prop;
}
}, {});
switch (part.stye.fore) {
case null: break;
case 'r': prop.color = 'red'; break;
case 'g': prop.color = 'green'; break;
case 'b': prop.color = 'blue'; break;
case 'c': prop.color = 'cyan'; break;
case 'm': prop.color = 'purple'; break;
case 'y': prop.color = 'yellow'; break;
case 'k': prop.color = 'black'; break;
case 'w': prop.color = 'white'; break;
default: prop.color = '#' + part.stye.fore;
}
switch (part.stye.back) {
case null: break;
case 'r': prop.backgroundColor = 'red'; break;
case 'g': prop.backgroundColor = 'green'; break;
case 'b': prop.backgroundColor = 'blue'; break;
case 'c': prop.backgroundColor = 'cyan'; break;
case 'm': prop.backgroundColor = 'purple'; break;
case 'y': prop.backgroundColor = 'yellow'; break;
case 'k': prop.backgroundColor = 'black'; break;
case 'w': prop.backgroundColor = 'white'; break;
default: prop.backgroundColor = '#' + part.stye.back;
}
if (Object.keys(prop).length === 0) {
return part.text;
} else {
return (<Text mono fontSize='inherit' key={i} {...prop}>
{part.text.join('')}
</Text>);
}
});
}
// render line
//
return (
<Text mono display='flex'
fontSize={0}
style={{ overflowWrap: 'break-word', whiteSpace: 'pre-wrap' }}
>
{text}
</Text>
);
});

View File

@ -1,23 +0,0 @@
body, #root {
height: 100vh;
margin: 0;
padding: 0;
}
input#term {
background-color: inherit;
color: inherit;
border: none;
}
.blink {
animation: 4s ease-in-out infinite opacity_blink;
}
@keyframes opacity_blink {
0% { opacity: 0; }
10% { opacity: 1; }
80% { opacity: 1; }
90% { opacity: 0; }
100% { opacity: 0; }
}

View File

@ -8,8 +8,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="apple-touch-icon" href="/~landscape/img/touch_icon.png">
<link rel="icon" type="image/png" href="/~landscape/img/Favicon.png">
<!--<link rel="apple-touch-icon" href="/~landscape/img/touch_icon.png">
<link rel="icon" type="image/png" href="/~landscape/img/Favicon.png">-->
<link rel="manifest"
href='data:application/manifest+json,{
"name": "Terminal",
@ -18,6 +18,16 @@
"display": "standalone",
"background_color": "%23FFFFFF",
"theme_color": "%23000000"}' />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet">
<style>
body, #root {
height: 100vh;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root"></div>

View File

@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
import { useTheme } from './settings';
import useTermState from './state';
export function useDark() {
const [osDark, setOsDark] = useState(false);
useEffect(() => {
const themeWatcher = window.matchMedia('(prefers-color-scheme: dark)');
const update = (e: MediaQueryListEvent) => {
setOsDark(e.matches);
};
setOsDark(themeWatcher.matches);
themeWatcher.addListener(update);
return () => {
themeWatcher.removeListener(update);
}
}, []);
const theme = useTermState(s => s.theme);
return theme === 'dark' || (osDark && theme === 'auto');
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,290 +0,0 @@
export default class Channel {
constructor() {
this.init();
this.deleteOnUnload();
// a way to handle channel errors
//
//
this.onChannelError = (err) => {
console.error('event source error: ', err);
};
this.onChannelOpen = (e) => {
console.log('open', e);
};
}
init() {
this.debounceInterval = 500;
// unique identifier: current time and random number
//
this.uid =
new Date().getTime().toString() +
"-" +
Math.random().toString(16).slice(-6);
this.requestId = 1;
// the currently connected EventSource
//
this.eventSource = null;
// the id of the last EventSource event we received
//
this.lastEventId = 0;
// this last event id acknowledgment sent to the server
//
this.lastAcknowledgedEventId = 0;
// a registry of requestId to successFunc/failureFunc
//
// These functions are registered during a +poke and are executed
// in the onServerEvent()/onServerError() callbacks. Only one of
// the functions will be called, and the outstanding poke will be
// removed after calling the success or failure function.
//
this.outstandingPokes = new Map();
// a registry of requestId to subscription functions.
//
// These functions are registered during a +subscribe and are
// executed in the onServerEvent()/onServerError() callbacks. The
// event function will be called whenever a new piece of data on this
// subscription is available, which may be 0, 1, or many times. The
// disconnect function may be called exactly once.
//
this.outstandingSubscriptions = new Map();
this.outstandingJSON = [];
this.debounceTimer = null;
}
resetDebounceTimer() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.debounceTimer = setTimeout(() => {
this.sendJSONToChannel();
}, this.debounceInterval)
}
setOnChannelError(onError = (err) => {}) {
this.onChannelError = onError;
}
setOnChannelOpen(onOpen = (e) => {}) {
this.onChannelOpen = onOpen;
}
deleteOnUnload() {
window.addEventListener("beforeunload", (event) => {
this.delete();
});
}
clearQueue() {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
this.sendJSONToChannel();
}
// sends a poke to an app on an urbit ship
//
poke(ship, app, mark, json, successFunc, failureFunc) {
let id = this.nextId();
this.outstandingPokes.set(
id,
{
success: successFunc,
fail: failureFunc
}
);
const j = {
id,
action: "poke",
ship,
app,
mark,
json
};
this.sendJSONToChannel(j);
}
// subscribes to a path on an specific app and ship.
//
// Returns a subscription id, which is the same as the same internal id
// passed to your Urbit.
subscribe(
ship,
app,
path,
connectionErrFunc = () => {},
eventFunc = () => {},
quitFunc = () => {},
subAckFunc = () => {},
) {
let id = this.nextId();
this.outstandingSubscriptions.set(
id,
{
err: connectionErrFunc,
event: eventFunc,
quit: quitFunc,
subAck: subAckFunc
}
);
const json = {
id,
action: "subscribe",
ship,
app,
path
}
this.resetDebounceTimer();
this.outstandingJSON.push(json);
return id;
}
// quit the channel
//
delete() {
let id = this.nextId();
clearInterval(this.ackTimer);
navigator.sendBeacon(this.channelURL(), JSON.stringify([{
id,
action: "delete"
}]));
if (this.eventSource) {
this.eventSource.close();
}
}
// unsubscribe to a specific subscription
//
unsubscribe(subscription) {
let id = this.nextId();
this.sendJSONToChannel({
id,
action: "unsubscribe",
subscription
});
}
// sends a JSON command command to the server.
//
sendJSONToChannel(j) {
let req = new XMLHttpRequest();
req.open("PUT", this.channelURL());
req.setRequestHeader("Content-Type", "application/json");
if (this.lastEventId == this.lastAcknowledgedEventId) {
if (j) {
this.outstandingJSON.push(j);
}
if (this.outstandingJSON.length > 0) {
let x = JSON.stringify(this.outstandingJSON);
req.send(x);
}
} else {
// we add an acknowledgment to clear the server side queue
//
// The server side puts messages it sends us in a queue until we
// acknowledge that we received it.
//
let payload = [
...this.outstandingJSON,
{action: "ack", "event-id": this.lastEventId}
];
if (j) {
payload.push(j)
}
let x = JSON.stringify(payload);
req.send(x);
this.lastAcknowledgedEventId = this.lastEventId;
}
this.outstandingJSON = [];
this.connectIfDisconnected();
}
// connects to the EventSource if we are not currently connected
//
connectIfDisconnected() {
if (this.eventSource) {
return;
}
this.eventSource = new EventSource(this.channelURL(), {withCredentials:true});
this.eventSource.onmessage = e => {
this.lastEventId = parseInt(e.lastEventId, 10);
let obj = JSON.parse(e.data);
let pokeFuncs = this.outstandingPokes.get(obj.id);
let subFuncs = this.outstandingSubscriptions.get(obj.id);
if (obj.response == "poke" && !!pokeFuncs) {
let funcs = pokeFuncs;
if (obj.hasOwnProperty("ok")) {
funcs["success"]();
} else if (obj.hasOwnProperty("err")) {
funcs["fail"](obj.err);
} else {
console.error("Invalid poke response: ", obj);
}
this.outstandingPokes.delete(obj.id);
} else if (obj.response == "subscribe" ||
(obj.response == "poke" && !!subFuncs)) {
let funcs = subFuncs;
if (obj.hasOwnProperty("err")) {
funcs["err"](obj.err);
this.outstandingSubscriptions.delete(obj.id);
} else if (obj.hasOwnProperty("ok")) {
funcs["subAck"](obj);
}
} else if (obj.response == "diff") {
// ensure we ack before channel clogs
if((this.lastEventId - this.lastAcknowledgedEventId) > 30) {
this.clearQueue();
}
let funcs = subFuncs;
funcs["event"](obj.json);
} else if (obj.response == "quit") {
let funcs = subFuncs;
funcs["quit"](obj);
this.outstandingSubscriptions.delete(obj.id);
} else {
console.log("Unrecognized response: ", e);
}
}
this.eventSource.onopen = this.onChannelOpen;
this.eventSource.onerror = e => {
this.delete();
this.init();
this.onChannelError(e);
}
}
channelURL() {
return "/~/channel/" + this.uid;
}
nextId() {
return this.requestId++;
}
}

Binary file not shown.

View File

@ -1,63 +1,23 @@
{
"name": "interface",
"name": "webterm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@reach/disclosure": "^0.10.5",
"@reach/menu-button": "^0.10.5",
"@reach/tabs": "^0.10.5",
"@react-spring/web": "^9.1.1",
"@tlon/indigo-dark": "^1.0.6",
"@tlon/indigo-light": "^1.0.7",
"@tlon/indigo-react": "^1.2.23",
"@tlon/sigil-js": "^1.4.3",
"@urbit/api": "^1.1.1",
"@urbit/http-api": "^1.2.1",
"any-ascii": "^0.1.7",
"aws-sdk": "^2.830.0",
"big-integer": "^1.6.48",
"classnames": "^2.2.6",
"codemirror": "^5.59.2",
"css-loader": "^3.6.0",
"file-saver": "^2.0.5",
"formik": "^2.1.5",
"immer": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0",
"normalize-wheel": "1.0.1",
"oembed-parser": "^1.4.5",
"prop-types": "^15.7.2",
"querystring": "^0.2.0",
"react": "^16.14.0",
"react-codemirror2": "^6.0.1",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react-markdown": "^4.3.1",
"react-oembed-container": "^1.0.0",
"react-router-dom": "^5.2.0",
"react-use-gesture": "^9.1.3",
"react-virtuoso": "^0.20.3",
"react-visibility-sensor": "^5.1.1",
"remark": "^12.0.0",
"remark-breaks": "^2.0.2",
"remark-disable-tokenizers": "1.1.0",
"stacktrace-js": "^2.0.2",
"style-loader": "^1.3.0",
"styled-components": "^5.1.1",
"styled-system": "^5.1.5",
"suncalc": "^1.8.0",
"unist-util-visit": "^3.0.0",
"urbit-ob": "^5.0.1",
"workbox-core": "^6.0.2",
"workbox-precaching": "^6.0.2",
"workbox-recipes": "^6.0.2",
"workbox-routing": "^6.0.2",
"yup": "^0.29.3",
"xterm": "^4.15.0",
"xterm-addon-fit": "^0.5.0",
"zustand": "^3.5.0"
},
"devDependencies": {
@ -69,17 +29,11 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@storybook/addon-actions": "^6.2.9",
"@storybook/addon-essentials": "^6.2.9",
"@storybook/addon-links": "^6.2.9",
"@storybook/react": "^6.2.9",
"@types/lodash": "^4.14.168",
"@types/react": "^16.14.2",
"@types/react-dom": "^16.9.10",
"@types/react-router-dom": "^5.1.7",
"@types/styled-components": "^5.1.7",
"@types/styled-system": "^5.1.10",
"@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.15.0",
"@typescript-eslint/parser": "^4.24.0",
"@urbit/eslint-config": "^1.0.0",
@ -87,9 +41,7 @@
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-root-import": "^6.6.0",
"chromatic": "^5.8.3",
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
@ -99,13 +51,7 @@
"husky": "^6.0.0",
"jest": "^26.6.3",
"lint-staged": "^11.0.0",
"loki": "^0.28.1",
"moment-locales-webpack-plugin": "^1.2.0",
"react-hot-loader": "^4.13.0",
"sass": "^1.32.5",
"sass-loader": "^8.0.2",
"storybook-addon-designs": "^6.0.0",
"ts-mdast": "^1.0.0",
"typescript": "^4.2.4",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
@ -121,9 +67,6 @@
"start": "webpack-dev-server --config config/webpack.dev.js",
"test": "tsc && jest",
"jest": "jest",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
"chromatic": "chromatic --exit-zero-on-changes",
"hook-lint": "eslint --cache --fix"
},
"author": "",

View File

@ -0,0 +1,26 @@
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import create from 'zustand';
import produce from 'immer';
type Session = { term: Terminal, fit: FitAddon };
type Sessions = { [id: string]: Session; }
export interface TermState {
sessions: Sessions,
selected: string,
slogstream: null | EventSource,
theme: 'auto' | 'light' | 'dark'
};
const useTermState = create<TermState>((set, get) => ({
sessions: {} as Sessions,
selected: '', // empty string is default session
slogstream: null,
theme: 'auto',
set: (f: (draft: TermState) => void) => {
set(produce(f));
}
} as TermState));
export default useTermState;

View File

@ -1,94 +0,0 @@
import { saveAs } from 'file-saver';
import bel from './lib/bel';
export default class Store {
state: any;
api: any;
setState: any;
constructor() {
this.state = this.initialState();
}
initialState() {
return {
lines: [''],
cursor: 0
};
}
clear() {
this.setState(this.initialState());
}
handleEvent(data) {
// process slogs
//
if (data.slog) {
this.state.lines.splice(this.state.lines.length-1, 0, { lin: [data.slog] });
this.setState({ lines: this.state.lines });
return;
}
// process blits
//
const blit = data.data;
switch (Object.keys(blit)[0]) {
case 'bel':
bel.play();
break;
case 'clr':
this.state.lines = this.state.lines.slice(-1);
this.setState({ lines: this.state.lines });
break;
case 'hop':
// since lines are lists of characters that might span multiple
// codepoints, we need to calculate the byte-wise cursor position
// to avoid incorrect cursor rendering.
//
const line = this.state.lines[this.state.lines.length - 1];
let hops;
if (line.lin) {
hops = line.lin.slice(0, blit.hop);
} else if (line.klr) {
hops = line.klr.reduce((h, p) => {
if (h.length >= blit.hop)
return h;
return [...h, ...p.text.slice(0, blit.hop - h.length)];
}, []);
}
this.setState({ cursor: hops.join('').length });
break;
case 'lin':
this.state.lines[this.state.lines.length - 1] = blit;
this.setState({ lines: this.state.lines });
break;
case 'klr':
this.state.lines[this.state.lines.length - 1] = blit;
this.setState({ lines: this.state.lines });
break;
case 'mor':
this.state.lines.push('');
this.setState({ lines: this.state.lines });
break;
case 'sag':
blit.sav = blit.sag;
break;
case 'sav':
const name = blit.sav.path.split('/').slice(-2).join('.');
const buff = new Buffer(blit.sav.file, 'base64');
const blob = new Blob([buff], { type: 'application/octet-stream' });
saveAs(blob, name);
break;
case 'url':
// TODO too invasive? just print as <a>?
window.open(blit.url);
break;
default: console.log('weird blit', blit);
}
}
setStateHandler(setState) {
this.setState = setState;
}
}

View File

@ -1,87 +0,0 @@
export default class Subscription {
store: any;
api: any;
channel: any;
firstRoundComplete: boolean;
constructor(store, api, channel) {
this.store = store;
this.api = api;
this.channel = channel;
this.channel.setOnChannelError(this.onChannelError.bind(this));
this.firstRoundComplete = false;
}
start() {
if (this.api.ship) {
this.firstRound();
} else {
console.error('~~~ ERROR: Must set api.ship before operation ~~~');
}
this.setupSlog();
}
setupSlog() {
let available = false;
const slog = new EventSource('/~_~/slog', { withCredentials: true });
slog.onopen = (e) => {
console.log('slog: opened stream');
available = true;
};
slog.onmessage = (e) => {
this.handleEvent({ slog: e.data });
};
slog.onerror = (e) => {
console.error('slog: eventsource error:', e);
if (available) {
window.setTimeout(() => {
if (slog.readyState !== EventSource.CLOSED)
return;
console.log('slog: reconnecting...');
this.setupSlog();
}, 10000);
}
};
}
delete() {
this.channel.delete();
}
onChannelError(err) {
console.error('event source error: ', err);
console.log('initiating new channel');
this.firstRoundComplete = false;
setTimeout(() => {
this.store.handleEvent({
data: { clear : true }
});
this.start();
}, 2000);
}
subscribe(path, app) {
this.api.bind(path, 'PUT', this.api.ship, app,
this.handleEvent.bind(this),
(err) => {
console.log(err);
this.subscribe(path, app);
},
() => {
this.subscribe(path, app);
});
}
firstRound() {
this.subscribe('/session/', 'herm');
}
handleEvent(diff) {
this.store.handleEvent(diff);
}
}

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './lib';

22
pkg/npm/api/term/lib.ts Normal file
View File

@ -0,0 +1,22 @@
import { Scry } from '../http-api/src'
import { Poke } from '../http-api/src/types';
import { Belt, Task, SessionTask } from './types';
export const pokeTask = (session: string, task: Task): Poke<SessionTask> => ({
app: 'herm',
mark: 'herm-task',
json: { session, ...task }
});
export const pokeBelt = (
session: string,
belt: Belt
): Poke<SessionTask> => pokeTask(session, { belt });
//NOTE scry will return string[]
// export const scrySessions = (): Scry => ({
// app: 'herm',
// path: `/sessions`
// });
//TODO remove stub once new backend lands
export const scrySessions = (): string[] => [''];

65
pkg/npm/api/term/types.ts Normal file
View File

@ -0,0 +1,65 @@
// outputs
//
export type TermUpdate =
| Blit;
export type Tint =
| null
| 'r' | 'g' | 'b' | 'c' | 'm' | 'y' | 'k' | 'w'
| { r: number, g: number, b: number };
export type Deco = null | 'br' | 'un' | 'bl';
export type Stye = {
deco: Deco[],
back: Tint,
fore: Tint
};
export type Stub = {
stye: Stye,
text: string[]
}
export type Blit =
| { bel: null } // make a noise
| { clr: null } // clear the screen
| { hop: number | { r: number, c: number } } // set cursor col/pos
| { klr: Stub[] } // put styled
| { put: string[] } // put text at cursor
| { nel: null } // newline
| { sag: { path: string, file: string } } // save to jamfile
| { sav: { path: string, file: string } } // save to file
| { url: string } // activate url
| { wyp: null } // wipe cursor line
//
| { lin: string[] } // legacy put
| { mor: true } // legacy nel
// inputs
//
export type Bolt =
| string
| { aro: 'd' | 'l' | 'r' | 'u' }
| { bac: null }
| { del: null }
| { hit: { r: number, c: number } }
| { ret: null }
export type Belt =
| Bolt
| { mod: { mod: 'ctl' | 'met' | 'hyp', key: Bolt } }
| { txt: Array<string> }
//
| { ctl: string }; // legacy mod
export type Task =
| { belt: Belt }
| { blew: { w: number, h: number } }
| { flow: { term: string, apps: Array<{ who: string, app: string }> } }
| { hail: null }
| { hook: null }
export type SessionTask = { session: string } & Task

View File

@ -1,9 +1,9 @@
:~ title+'Terminal'
info+'A web interface to your Urbit\'s command line (the dojo).'
info+'A web interface to your Urbit\'s command line.'
color+0x2e.4347
glob-http+['https://bootstrap.urbit.org/glob-0v6.ak34j.nao8k.dhqs4.s2atf.td1lc.glob' 0v6.ak34j.nao8k.dhqs4.s2atf.td1lc]
glob-http+['https://bootstrap.urbit.org/glob-0v1.fgmgl.utdgt.kdu3r.4e5f9.v58rk.glob' 0v1.fgmgl.utdgt.kdu3r.4e5f9.v58rk]
base+'webterm'
version+[0 0 1]
version+[1 0 0]
website+'https://tlon.io'
license+'MIT'
==

1
sh/fmt
View File

@ -10,6 +10,5 @@ cd "${0%/*}"
./fmt-shell
./fmt-nix
./fmt-haskell
echo 'Done.'

View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
# Usage: fmt-haskell
set -euo pipefail
cd "${0%/*}/.."
echo "Formatting Haskell"
# FIXME: Avoid inplace (-i) modifications for now.
find pkg/hs -type f -name '*.hs' \
-exec ormolu --mode check \
-o '-XBangPatterns' \
-o '-XMagicHash' \
-o '-XTypeApplications' \
-o '-XPatternSynonyms' \
{} \+

View File

@ -9,4 +9,4 @@ cd "${0%/*}/.."
echo "Formatting Nix"
find . -type f -name '*.nix' \
-exec nixfmt {} \+
-exec nixpkgs-fmt {} \+

View File

@ -1,5 +1,5 @@
# A repository wide shell.nix containing all tools, formatters, and inputs
# required to build any of the C or Haskell packages.
# required to build any of the C packages.
#
# Entering a nix-shell using this derivation will allow you to cd anywhere
# in the ./pkg directory and run the appropriate build tooling.
@ -14,48 +14,25 @@ let
pkgsLocal = import ./default.nix { };
# The non-Haskell packages which build inputs (dependencies) will be
# propagated into the shell. This combines nixpkgs' mkShell behaviour
# with Haskell.nix's shellFor.
# The packages from which build inputs (dependencies) will be propagated into
# the shell.
#
# For example, adding urbit here results in gmp, h2o, zlib, etc. being
# made available, so you can just run make.
#
#
# Typically the inputs listed here also have a shell.nix in their respective
# source directory you can use, to avoid the Haskell/GHC dependencies.
# source directory you can use directly.
inputsFrom = with pkgsLocal; [ ent herb urbit urcrypt ];
# Collect the named attribute from all dependencies listed in inputsFrom.
mergeFrom = name: pkgs.lib.concatLists (pkgs.lib.catAttrs name inputsFrom);
in pkgsLocal.hs.shellFor {
# Haskell packages from the stackProject which will have their
# dependencies available in the shell.
packages = ps:
with ps; [
racquire
terminal-progress-bar
urbit-atom
urbit-azimuth
urbit-eventlog-lmdb
urbit-king
urbit-noun
urbit-noun-core
urbit-termsize
];
# Haskell tools to make available on the shell's PATH.
tools = {
shellcheck = "0.7.1";
ormolu = "0.1.3.0";
};
in pkgs.mkShell {
# Nixpkgs tools to make available on the shell's PATH.
buildInputs = [
pkgs.cacert
pkgs.nixfmt
pkgs.nixpkgs-fmt
pkgs.shfmt
pkgs.stack
(import pkgs.sources.niv { }).niv
] ++ mergeFrom "buildInputs";