Squashed commit of the following:

commit b4f616a2c9
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 17:02:29 2023 +0200

    fix bash

commit 50b5f5083d
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 16:10:27 2023 +0200

    bash

commit dffa78e3dc
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 15:10:28 2023 +0200

    shells

commit f34a4e394e
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 14:26:48 2023 +0200

    update readme

commit f53cba9205
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 14:18:57 2023 +0200

    static

commit 897e81246f
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 14:01:07 2023 +0200

    Update README.md

commit 5d7e112a35
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 13:56:34 2023 +0200

    rm txt file

commit 0d77e68ddf
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 13:55:40 2023 +0200

    add readme

commit b7f59b65ac
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jul 4 13:54:52 2023 +0200

    fix glob

commit 962395926e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jul 4 12:50:44 2023 +0200

    fix modals

commit 3e610699cc
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jul 4 12:18:01 2023 +0200

    commits

commit c0cc7328ef
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jul 4 10:57:11 2023 +0200

    fix class

commit d2eea4eea2
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jul 4 10:39:53 2023 +0200

    linting

commit 673258ef87
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jul 4 10:11:57 2023 +0200

    filter

commit 0257e2928e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jul 4 10:10:13 2023 +0200

    ci filter

commit 329de0d339
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jul 4 10:06:40 2023 +0200

    fix lint

commit 29abc4076e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jul 4 09:27:31 2023 +0200

    install yarn

commit 366fd4969e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 17:33:56 2023 +0200

    yarn

commit ea7e69faa3
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:52:08 2023 +0200

    console

commit 3b6561c488
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:51:44 2023 +0200

    consoles

commit 57ef21ab94
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:50:20 2023 +0200

    consoles

commit 867b9775c0
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:49:54 2023 +0200

    consoles

commit a2dc8b996a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:47:14 2023 +0200

    rm lock

commit bc6d4d23a3
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:46:16 2023 +0200

    restore libs

commit 1a76ab87ba
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:44:40 2023 +0200

    fix git

commit 870083ff49
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:29:33 2023 +0200

    typo

commit 8987d73d20
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:26:14 2023 +0200

    can use worker

commit 6045655cd2
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:26:07 2023 +0200

    can use worker

commit 5ee444e030
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:13:38 2023 +0200

    cleanup package

commit 7f5f0bfd37
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:10:01 2023 +0200

    fix lib

commit 1d97df570c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:05:26 2023 +0200

    rm test app

commit 0483d13e56
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jul 3 16:03:17 2023 +0200

    logs

commit 8edd1925b7
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 28 19:53:27 2023 +0200

    debugger

commit 16f337a2d7
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 28 16:36:44 2023 +0200

    fix env shell

commit 35a7690591
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 28 16:15:54 2023 +0200

    terminal menu

commit 12c0894079
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 28 13:26:23 2023 +0200

    custom components

commit 731aecd556
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 28 12:39:36 2023 +0200

    terminals

commit 297c476b21
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 28 12:39:32 2023 +0200

    terminals

commit aeebd08602
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 15:57:26 2023 +0200

    bugfix

commit c104f7056a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 15:54:07 2023 +0200

    rm ripgrep

commit 9c3283c017
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 15:01:41 2023 +0200

    xterm panels

commit e0a2f62f71
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jun 27 13:51:09 2023 +0200

    terminals

commit 1607d4a586
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jun 27 13:02:36 2023 +0200

    pre18

commit f4e3dfc01d
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Tue Jun 27 12:29:16 2023 +0200

    xterm build

commit cb32ecbd31
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 10:21:47 2023 +0200

    ripgrep tests

commit ee56ee6a0e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:41:05 2023 +0200

    machine image

commit f5bdd715ce
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:34:32 2023 +0200

    14.17.6

commit 5af1131851
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:26:58 2023 +0200

    add orb

commit 6897b22cad
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:25:06 2023 +0200

    node install

commit 84e58d7ad0
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:21:03 2023 +0200

    18.04

commit 716e39f00f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:12:05 2023 +0200

    docker

commit 26006c6e2a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:04:35 2023 +0200

    typo

commit 48444e18bf
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:03:13 2023 +0200

    machine

commit bf007bb1d1
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 08:01:54 2023 +0200

    electronuserland/builder:14

commit 80e367e827
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 07:56:17 2023 +0200

    current

commit fcc0366600
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 07:52:28 2023 +0200

    run

commit 5a76be67fa
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 27 07:51:20 2023 +0200

    linux

commit 3b04e0df7c
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Mon Jun 26 18:06:15 2023 +0200

    ripgrep

commit f927730171
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jun 26 17:26:36 2023 +0200

    fixes for git

commit e5ec564b1e
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Mon Jun 26 12:11:29 2023 +0200

    git fixes

commit cc3d9ea5fb
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 17:30:19 2023 +0200

    less logs

commit dc8cf19bf1
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 16:51:27 2023 +0200

    use native git

commit fe86f5927b
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 14:27:19 2023 +0200

    icon

commit 76244314f4
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 14:10:51 2023 +0200

    no asar

commit f31ac8e008
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 13:51:41 2023 +0200

    linux fields

commit 30c186d20b
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 13:42:38 2023 +0200

    author

commit 2035636e20
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 13:36:00 2023 +0200

    email

commit 7a28db2f52
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 13:23:27 2023 +0200

    linux

commit 6d8b169c30
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 13:22:28 2023 +0200

    config

commit a1050f1714
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 13:21:41 2023 +0200

    linux

commit 0728709ce2
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 13:04:43 2023 +0200

    desktopbuild

commit 539d992108
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 13:00:52 2023 +0200

    unzip

commit ec52d9acf3
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:54:52 2023 +0200

    typo

commit 3690a7b314
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:50:29 2023 +0200

    typo

commit 7880ff4e36
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:47:15 2023 +0200

    windows

commit 3424201925
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:45:26 2023 +0200

    config

commit 1ad75731f7
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:41:34 2023 +0200

    config

commit 8fa20b74a4
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:41:11 2023 +0200

    config

commit be174d3da3
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:40:35 2023 +0200

    config

commit 32bbaf31bc
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:35:18 2023 +0200

    config

commit 1ec69c6e1e
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:34:52 2023 +0200

    config

commit acca77164c
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:33:25 2023 +0200

    build desktop

commit 6ecd1e8b2e
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:23:44 2023 +0200

    cache

commit b75983af55
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:15:18 2023 +0200

    ci

commit 9b4b8970ae
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 12:13:27 2023 +0200

    fix path

commit 34acdb76a1
Author: bunsenstraat <bunsenstraat@gmail.com>
Date:   Sun Jun 25 11:28:41 2023 +0200

    fix windows

commit 0de6b9cc74
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Sat Jun 24 14:22:09 2023 +0200

    es6

commit 7a51a12912
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Sat Jun 24 09:49:07 2023 +0200

    USE_HARD_LINKS

commit f77e8cb9a4
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri Jun 23 20:03:26 2023 +0200

    mkdir

commit 16b492cb20
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri Jun 23 19:58:24 2023 +0200

    m1

commit 2daccdec42
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 17:22:31 2023 +0200

    revert

commit 0e1c48d3a1
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 17:12:09 2023 +0200

    targets

commit 3187054eb7
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 16:59:52 2023 +0200

    large

commit e0765f2b1e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 16:58:53 2023 +0200

    large

commit d2b9e1b557
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 16:58:19 2023 +0200

    xlarge

commit a2d6acaab8
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 16:41:29 2023 +0200

    cmd

commit 6108f51031
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 16:35:11 2023 +0200

    windows

commit 9f68bdf415
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 16:24:27 2023 +0200

    package

commit bf57415b8d
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 16:09:00 2023 +0200

    apps/remixdesktop/

commit f8b6de1cfc
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 16:03:46 2023 +0200

    store

commit 02646d2f19
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 15:41:19 2023 +0200

    other glob version

commit f77e18dfa5
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 15:19:07 2023 +0200

    rm lock

commit 276760eeee
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 15:15:43 2023 +0200

    path

commit ad8811c068
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 15:14:04 2023 +0200

    orb

commit 016b29a94b
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 15:13:26 2023 +0200

    job

commit 2a115aa6e8
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 15:12:22 2023 +0200

    windows test

commit 570fdd3ab3
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 12:54:32 2023 +0200

    typo

commit cb41e83dec
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 12:49:52 2023 +0200

    mdkir

commit 24908d0ffa
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 12:35:41 2023 +0200

    lock

commit f67726f191
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 12:29:11 2023 +0200

    lock

commit 90c6f5e1d9
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 12:12:35 2023 +0200

    20

commit 99d9b6cc33
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 12:05:21 2023 +0200

    use20

commit 6053f2d38a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 11:54:05 2023 +0200

    space

commit eb6d01edb7
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 11:51:12 2023 +0200

    ls

commit abedb4a95d
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 11:50:09 2023 +0200

    node v

commit 0b818ca9fd
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 11:47:51 2023 +0200

    ci

commit cca1a7d63d
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 11:44:12 2023 +0200

    build CI

commit 07c392e0ca
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Jun 23 10:31:42 2023 +0200

    gist fix

commit 9ecc3949df
Merge: 54ffcd5dc c7dbcf15c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 14:42:55 2023 +0200

    Merge branch 'rdesktop' of https://github.com/ethereum/remix-project into rdesktop

commit 54ffcd5dc3
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 14:38:00 2023 +0200

    fix

commit cedb19ed9e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 14:01:54 2023 +0200

    remixd test

commit 7f13f5d797
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:57:37 2023 +0200

    fix test

commit 2216cebb4b
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:56:08 2023 +0200

    fix test

commit 5dcfef2820
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:45:36 2023 +0200

    required modules

commit 5c8a92ac0f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:20:12 2023 +0200

    remove recent folder

commit a69a6e648a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:04:35 2023 +0200

    filechanged

commit 3ce2e5b200
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 10:12:59 2023 +0200

    context menu

commit 570658f54c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 09:34:53 2023 +0200

    menu

commit 3660c8bc33
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 08:44:51 2023 +0200

    fix menu

commit a1dceb706d
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 08:21:41 2023 +0200

    hometab & clone

commit e58d67345b
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 21 09:24:15 2023 +0200

    provider events

commit 356d8ed3dc
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 20 20:24:44 2023 +0200

    some git functions

commit ee259cd49f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 20 17:34:31 2023 +0200

    fix search

commit 193230f675
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 20 16:31:17 2023 +0200

    glob

commit f3ec824c62
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 14 15:45:55 2023 +0200

    folder handling

commit b888c2b894
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue Jun 13 17:16:36 2023 +0200

    open folder

commit eaefe37861
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 13 12:51:12 2023 +0200

    loading engine

commit 5728f3c7db
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 13 07:47:33 2023 +0200

    isogit

commit 429e845785
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 19:39:39 2023 +0200

    dgit

commit b106cc8cdf
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 17:22:30 2023 +0200

    fs support

commit ac28db3a52
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 13:33:28 2023 +0200

    refactor

commit 892915b138
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 11:54:41 2023 +0200

    fs integration

commit 218a56bdc7
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 11:54:27 2023 +0200

    update test1

commit 6c25d81166
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 11:49:03 2023 +0200

    update test app

commit 24039ebb43
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat Jun 3 13:42:10 2023 +0200

    other builder

commit a5c71f4379
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri Jun 2 13:45:26 2023 +0200

    cleanup

commit 69398c09af
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri Jun 2 13:33:38 2023 +0200

    terminals

commit 89c146f1de
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 18:07:07 2023 +0200

    serve

commit 6bb9beb950
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:52:42 2023 +0200

    engine:activatePlugin

commit 3b26898fb1
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:50:57 2023 +0200

    refactor

commit 881d0c1b12
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:47:48 2023 +0200

    rename

commit c28f72bca6
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:46:39 2023 +0200

    cleanup

commit 96df90fa1c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:43:04 2023 +0200

    refactor

commit 92feaa0283
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:32:35 2023 +0200

    close watcher

commit bea74d4124
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:28:33 2023 +0200

    close watcher

commit 6778a113f8
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 10:47:55 2023 +0200

    refactor

commit 87f65eeba7
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 10:41:22 2023 +0200

    refactor

commit 971e4d0265
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 10:02:37 2023 +0200

    refactor

commit 78d3247725
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 00:39:47 2023 +0200

    change fs

commit ee4b672de3
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 00:29:36 2023 +0200

    add test app

commit 8287f68209
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue May 30 08:31:18 2023 +0200

    fix path

commit 47f3fa47a3
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Mon May 29 18:39:08 2023 +0200

    app build & serve

commit c65465b056
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Mon May 29 09:52:36 2023 +0200

    rm trash

commit 59210c07b5
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Sun May 28 10:56:08 2023 +0200

    new app

commit 463d39d99e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 15:29:49 2023 +0200

    fix webpack

commit f340185415
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 14:56:07 2023 +0200

    prepackage

commit 620ede28b0
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 13:56:08 2023 +0200

    testing

commit cca42ea2a5
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 13:38:54 2023 +0200

    dev server

commit 560cfc2371
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 13:17:18 2023 +0200

    experiments

commit c7dbcf15c2
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 14:01:54 2023 +0200

    remixd test

commit 367761fe7f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:57:37 2023 +0200

    fix test

commit 3a25960735
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:56:08 2023 +0200

    fix test

commit 3cd59ec57b
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:45:36 2023 +0200

    required modules

commit 6c0ffc29af
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:20:12 2023 +0200

    remove recent folder

commit f170b08344
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 13:04:35 2023 +0200

    filechanged

commit 68515019a9
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 10:12:59 2023 +0200

    context menu

commit 443dcde260
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 09:34:53 2023 +0200

    menu

commit 21e85f6881
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 08:44:51 2023 +0200

    fix menu

commit 1d36d50ab6
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 22 08:21:41 2023 +0200

    hometab & clone

commit f542fd8d1b
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 21 09:24:15 2023 +0200

    provider events

commit cdf0ca95c2
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 20 20:24:44 2023 +0200

    some git functions

commit 7dac9f1731
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 20 17:34:31 2023 +0200

    fix search

commit d1073ed322
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 20 16:31:17 2023 +0200

    glob

commit 71eef01e5d
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 14 15:45:55 2023 +0200

    folder handling

commit b8ed5556cb
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue Jun 13 17:16:36 2023 +0200

    open folder

commit 274a3c7cbd
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 13 12:51:12 2023 +0200

    loading engine

commit 01433bae4f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 13 07:47:33 2023 +0200

    isogit

commit 038c63e362
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 19:39:39 2023 +0200

    dgit

commit 336d191c20
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 17:22:30 2023 +0200

    fs support

commit 87c498fa91
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 13:33:28 2023 +0200

    refactor

commit 34abe45ebc
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 11:54:41 2023 +0200

    fs integration

commit 2e39267532
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 11:54:27 2023 +0200

    update test1

commit 6534e8aa47
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 7 11:49:03 2023 +0200

    update test app

commit 272bf13344
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat Jun 3 13:42:10 2023 +0200

    other builder

commit a2d511d445
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri Jun 2 13:45:26 2023 +0200

    cleanup

commit 15d1a0189c
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri Jun 2 13:33:38 2023 +0200

    terminals

commit 3cd5c54262
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 18:07:07 2023 +0200

    serve

commit b8e344da37
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:52:42 2023 +0200

    engine:activatePlugin

commit 05f5bad529
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:50:57 2023 +0200

    refactor

commit 12b51f44bd
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:47:48 2023 +0200

    rename

commit 5bff29ea3a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:46:39 2023 +0200

    cleanup

commit 0b878097cb
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:43:04 2023 +0200

    refactor

commit f68b189969
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:32:35 2023 +0200

    close watcher

commit 03163d303e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 11:28:33 2023 +0200

    close watcher

commit 946135bde0
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 10:47:55 2023 +0200

    refactor

commit eae0a0f696
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 10:41:22 2023 +0200

    refactor

commit 93973004e9
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 10:02:37 2023 +0200

    refactor

commit 171518f49a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 00:39:47 2023 +0200

    change fs

commit f275f0ae8a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 1 00:29:36 2023 +0200

    add test app

commit c9297a4d8f
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue May 30 08:31:18 2023 +0200

    fix path

commit acf2e01aa8
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Mon May 29 18:39:08 2023 +0200

    app build & serve

commit 4819477577
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Mon May 29 09:52:36 2023 +0200

    rm trash

commit b4c9657e9b
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Sun May 28 10:56:08 2023 +0200

    new app

commit 542e20ea9c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 15:29:49 2023 +0200

    fix webpack

commit 94c19ac6d4
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 14:56:07 2023 +0200

    prepackage

commit 2102b30044
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 13:56:08 2023 +0200

    testing

commit 8220097fb7
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 13:38:54 2023 +0200

    dev server

commit f966c4cb9b
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 27 13:17:18 2023 +0200

    experiments
pull/3885/head
bunsenstraat 1 year ago
parent ec495fb4f3
commit 753732a007
  1. 155
      .circleci/config.yml
  2. 4
      .gitignore
  3. 2
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  4. 6
      apps/remix-ide-e2e/src/tests/solidityImport.test.ts
  5. 9
      apps/remix-ide/contracts/.deps/remix-tests/remix_accounts.sol
  6. 225
      apps/remix-ide/contracts/.deps/remix-tests/remix_tests.sol
  7. 7
      apps/remix-ide/project.json
  8. 48
      apps/remix-ide/src/app.js
  9. 325
      apps/remix-ide/src/app/files/dgitProvider.ts
  10. 84
      apps/remix-ide/src/app/files/electronProvider.ts
  11. 30
      apps/remix-ide/src/app/files/fileManager.ts
  12. 37
      apps/remix-ide/src/app/files/fileProvider.ts
  13. 2
      apps/remix-ide/src/app/files/remixDProvider.js
  14. 2
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  15. 2
      apps/remix-ide/src/app/panels/file-panel.js
  16. 24
      apps/remix-ide/src/app/panels/terminal.js
  17. 19
      apps/remix-ide/src/app/plugins/electron/electronConfigPlugin.ts
  18. 143
      apps/remix-ide/src/app/plugins/electron/fsPlugin.ts
  19. 29
      apps/remix-ide/src/app/plugins/electron/isoGitPlugin.ts
  20. 15
      apps/remix-ide/src/app/plugins/electron/templatesPlugin.ts
  21. 11
      apps/remix-ide/src/app/plugins/electron/xtermPlugin.ts
  22. 3
      apps/remix-ide/src/app/plugins/parser/services/code-parser-antlr-service.ts
  23. 28
      apps/remix-ide/src/app/plugins/parser/services/code-parser-imports.ts
  24. 30
      apps/remix-ide/src/app/plugins/remix-templates.ts
  25. 1
      apps/remix-ide/src/app/plugins/remixd-handle.tsx
  26. 4
      apps/remix-ide/src/app/tabs/locales/en/electron.json
  27. 2
      apps/remix-ide/src/app/tabs/locales/en/index.js
  28. 30
      apps/remix-ide/src/app/tabs/theme-module.js
  29. 1
      apps/remix-ide/src/index.tsx
  30. 9
      apps/remix-ide/src/remixAppManager.js
  31. 2
      apps/remix-ide/src/remixEngine.js
  32. 15
      apps/remix-ide/webpack.config.js
  33. 22
      apps/remixdesktop/README.md
  34. BIN
      apps/remixdesktop/assets/icon.png
  35. 105
      apps/remixdesktop/package.json
  36. 61
      apps/remixdesktop/src/engine.ts
  37. 114
      apps/remixdesktop/src/main.ts
  38. 39
      apps/remixdesktop/src/menus/commands.ts
  39. 28
      apps/remixdesktop/src/menus/darwin.ts
  40. 53
      apps/remixdesktop/src/menus/edit.ts
  41. 49
      apps/remixdesktop/src/menus/file.ts
  42. 20
      apps/remixdesktop/src/menus/git.ts
  43. 26
      apps/remixdesktop/src/menus/main.ts
  44. 20
      apps/remixdesktop/src/menus/terminal.ts
  45. 87
      apps/remixdesktop/src/menus/view.ts
  46. 63
      apps/remixdesktop/src/menus/window.ts
  47. 50
      apps/remixdesktop/src/plugins/configPlugin.ts
  48. 377
      apps/remixdesktop/src/plugins/fsPlugin.ts
  49. 368
      apps/remixdesktop/src/plugins/isoGitPlugin.ts
  50. 72
      apps/remixdesktop/src/plugins/templates.ts
  51. 153
      apps/remixdesktop/src/plugins/xtermPlugin.ts
  52. 33
      apps/remixdesktop/src/preload.ts
  53. 151
      apps/remixdesktop/src/tools/git.ts
  54. 39
      apps/remixdesktop/src/utils/config.ts
  55. 86
      apps/remixdesktop/src/utils/findExecutable.ts
  56. 17
      apps/remixdesktop/tsconfig.json
  57. 5069
      apps/remixdesktop/yarn.lock
  58. 3
      libs/remix-core-plugin/src/lib/gist-handler.ts
  59. 1
      libs/remix-ui/app/src/lib/remix-app/remix-app.tsx
  60. 4
      libs/remix-ui/home-tab/src/lib/components/homeTabFeatured.tsx
  61. 2
      libs/remix-ui/home-tab/src/lib/components/homeTabFeaturedPlugins.tsx
  62. 41
      libs/remix-ui/home-tab/src/lib/components/homeTabFileElectron.tsx
  63. 7
      libs/remix-ui/home-tab/src/lib/components/homeTabGetStarted.tsx
  64. 6
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx
  65. 4
      libs/remix-ui/panel/src/lib/dragbar/dragbar.tsx
  66. 2
      libs/remix-ui/panel/src/lib/main/main-panel.css
  67. 1
      libs/remix-ui/search/src/lib/components/results/ResultItem.tsx
  68. 24
      libs/remix-ui/search/src/lib/components/results/SearchHelper.ts
  69. 5
      libs/remix-ui/search/src/lib/context/context.tsx
  70. 3
      libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx
  71. 3
      libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
  72. 12
      libs/remix-ui/workspace/src/lib/actions/events.ts
  73. 56
      libs/remix-ui/workspace/src/lib/actions/index.ts
  74. 7
      libs/remix-ui/workspace/src/lib/actions/payload.ts
  75. 73
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  76. 65
      libs/remix-ui/workspace/src/lib/components/electron-menu.tsx
  77. 4
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  78. 3
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  79. 27
      libs/remix-ui/workspace/src/lib/css/electron-menu.css
  80. 20
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  81. 18
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  82. 175
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  83. 2
      libs/remix-ui/workspace/src/lib/types/index.ts
  84. 21
      libs/remix-ui/workspace/src/lib/utils/index.ts
  85. 2
      libs/remix-ui/xterm/src/index.ts
  86. 48
      libs/remix-ui/xterm/src/lib/components/remix-ui-xterm.tsx
  87. 234
      libs/remix-ui/xterm/src/lib/components/remix-ui-xterminals.tsx
  88. 92
      libs/remix-ui/xterm/src/lib/components/xterm-fit-addOn.ts
  89. 237
      libs/remix-ui/xterm/src/lib/components/xterm-wrap.tsx
  90. 66
      libs/remix-ui/xterm/src/lib/css/index.css
  91. 35
      package.json
  92. 4
      tsconfig.json
  93. 4
      tsconfig.paths.json
  94. 1797
      yarn.lock

@ -6,6 +6,7 @@ parameters:
default: false default: false
orbs: orbs:
browser-tools: circleci/browser-tools@1.4.1 browser-tools: circleci/browser-tools@1.4.1
win: circleci/windows@5.0
jobs: jobs:
build: build:
docker: docker:
@ -50,6 +51,32 @@ jobs:
paths: paths:
- "persist" - "persist"
build-desktop:
docker:
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
steps:
- checkout
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn
- save_cache:
key: v1-deps-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run:
name: Build
command: |
yarn build:desktop
- run: mkdir persist && zip -0 -r persist/desktopbuild.zip dist/apps/remix-ide
- persist_to_workspace:
root: .
paths:
- "persist"
build-plugin: build-plugin:
docker: docker:
@ -77,6 +104,121 @@ jobs:
paths: paths:
- "persist" - "persist"
build-remixdesktop-linux:
machine:
image: ubuntu-2004:current
resource_class:
xlarge
working_directory: ~/remix-project
steps:
- run: ldd --version
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- restore_cache:
keys:
- remixdesktop-linux-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run:
command: |
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build/remix-ide
cd apps/remixdesktop/
yarn
yarn dist --linux
rm -rf release/*-unpacked
- save_cache:
key: remixdesktop-linux-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
- store_artifacts:
path: apps/remixdesktop/release/
destination: remixdesktop-linux
build-remixdesktop-windows:
executor:
name: win/default # executor type
size: xlarge # can be medium, large, xlarge, 2xlarge
shell: bash.exe
working_directory: ~/remix-project
steps:
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- restore_cache:
key: node-20-windows-v3
- run:
command: |
nvm install 20.0.0
nvm use 20.0.0
node -v
npx -v
npm install --global yarn
yarn -v
- save_cache:
key: node-20-windows-v3
paths:
- /ProgramData/nvm/v20.0.0
- restore_cache:
keys:
- remixdesktop-windows-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run:
command: |
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build/remix-ide
cd apps/remixdesktop/
yarn
yarn dist --win
rm -rf release/*-unpacked
- save_cache:
key: remixdesktop-windows-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
- store_artifacts:
path: apps/remixdesktop/release/
destination: remixdesktop-windows
build-remixdesktop-mac:
macos:
xcode: 14.2.0
resource_class:
macos.m1.large.gen1
working_directory: ~/remix-project
steps:
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
ls -la dist/apps/remix-ide
nvm install 20.0.0
nvm use 20.0.0
- restore_cache:
keys:
- remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run:
command: |
nvm use 20.0.0
cd apps/remixdesktop && yarn
- save_cache:
key: remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
# use USE_HARD_LINK=false https://github.com/electron-userland/electron-builder/issues/3179
- run:
command: |
nvm use 20.0.0
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build/remix-ide
cd apps/remixdesktop
USE_HARD_LINKS=false yarn dist --mac
rm -rf release/mac*
- store_artifacts:
path: apps/remixdesktop/release/
destination: remixdesktop-mac
lint: lint:
docker: docker:
- image: cimg/node:20.0.0-browsers - image: cimg/node:20.0.0-browsers
@ -287,6 +429,19 @@ workflows:
unless: << pipeline.parameters.run_flaky_tests >> unless: << pipeline.parameters.run_flaky_tests >>
jobs: jobs:
- build - build
- build-desktop:
filters:
branches:
only: ['master', /.*desktop.*/]
- build-remixdesktop-mac:
requires:
- build-desktop
- build-remixdesktop-windows:
requires:
- build-desktop
- build-remixdesktop-linux:
requires:
- build-desktop
- build-plugin: - build-plugin:
matrix: matrix:
parameters: parameters:

4
.gitignore vendored

@ -57,3 +57,7 @@ testem.log
.DS_Store .DS_Store
.vscode/settings.json .vscode/settings.json
.vscode/launch.json .vscode/launch.json
apps/remixdesktop/.webpack
apps/remixdesktop/out
apps/remixdesktop/release/

@ -104,7 +104,7 @@ module.exports = {
}) })
.addFile('test_import_node_modules_with_github_import.sol', sources[4]['test_import_node_modules_with_github_import.sol']) .addFile('test_import_node_modules_with_github_import.sol', sources[4]['test_import_node_modules_with_github_import.sol'])
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.setSolidityCompilerVersion('soljson-v0.8.19+commit.7dd6d404.js') // open-zeppelin moved to pragma ^0.8.0 .setSolidityCompilerVersion('soljson-v0.8.20+commit.a1b79de6.js') // open-zeppelin moved to pragma ^0.8.0 (master branch)
.testContracts('test_import_node_modules_with_github_import.sol', sources[4]['test_import_node_modules_with_github_import.sol'], ['ERC20', 'test11']) .testContracts('test_import_node_modules_with_github_import.sol', sources[4]['test_import_node_modules_with_github_import.sol'], ['ERC20', 'test11'])
}, },
'Static Analysis run with remixd #group3': '' + function (browser) { 'Static Analysis run with remixd #group3': '' + function (browser) {

@ -38,7 +38,7 @@ module.exports = {
'Test GitHub Import - from master branch #group1': function (browser: NightwatchBrowser) { 'Test GitHub Import - from master branch #group1': function (browser: NightwatchBrowser) {
browser browser
.setSolidityCompilerVersion('soljson-v0.8.19+commit.7dd6d404.js') // open-zeppelin moved to pragma ^0.8.19 (master branch) .setSolidityCompilerVersion('soljson-v0.8.20+commit.a1b79de6.js') // open-zeppelin moved to pragma ^0.8.0 (master branch)
.addFile('Untitled4.sol', sources[3]['Untitled4.sol']) .addFile('Untitled4.sol', sources[3]['Untitled4.sol'])
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.verifyContracts(['test7', 'ERC20'], { wait: 10000 }) .verifyContracts(['test7', 'ERC20'], { wait: 10000 })
@ -54,7 +54,7 @@ module.exports = {
'Test GitHub Import - no branch specified #group2': function (browser: NightwatchBrowser) { 'Test GitHub Import - no branch specified #group2': function (browser: NightwatchBrowser) {
browser browser
.setSolidityCompilerVersion('soljson-v0.8.19+commit.7dd6d404.js') // open-zeppelin moved to pragma ^0.8.19 (master branch) .setSolidityCompilerVersion('soljson-v0.8.20+commit.a1b79de6.js') // open-zeppelin moved to pragma ^0.8.0 (master branch)
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('li[data-id="treeViewLitreeViewItemREADME.txt"') .click('li[data-id="treeViewLitreeViewItemREADME.txt"')
.addFile('Untitled6.sol', sources[5]['Untitled6.sol']) .addFile('Untitled6.sol', sources[5]['Untitled6.sol'])
@ -64,7 +64,7 @@ module.exports = {
'Test GitHub Import - raw URL #group4': function (browser: NightwatchBrowser) { 'Test GitHub Import - raw URL #group4': function (browser: NightwatchBrowser) {
browser browser
.setSolidityCompilerVersion('soljson-v0.8.19+commit.7dd6d404.js') // open-zeppelin moved to pragma ^0.8.0 (master branch) .setSolidityCompilerVersion('soljson-v0.8.20+commit.a1b79de6.js') // open-zeppelin moved to pragma ^0.8.0 (master branch)
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('li[data-id="treeViewLitreeViewItemREADME.txt"') .click('li[data-id="treeViewLitreeViewItemREADME.txt"')
.addFile('Untitled7.sol', sources[6]['Untitled7.sol']) .addFile('Untitled7.sol', sources[6]['Untitled7.sol'])

@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
library TestsAccounts {
function getAccount(uint index) pure public returns (address) {
return address(0);
}
}

@ -0,0 +1,225 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
library Assert {
event AssertionEvent(
bool passed,
string message,
string methodName
);
event AssertionEventUint(
bool passed,
string message,
string methodName,
uint256 returned,
uint256 expected
);
event AssertionEventInt(
bool passed,
string message,
string methodName,
int256 returned,
int256 expected
);
event AssertionEventBool(
bool passed,
string message,
string methodName,
bool returned,
bool expected
);
event AssertionEventAddress(
bool passed,
string message,
string methodName,
address returned,
address expected
);
event AssertionEventBytes32(
bool passed,
string message,
string methodName,
bytes32 returned,
bytes32 expected
);
event AssertionEventString(
bool passed,
string message,
string methodName,
string returned,
string expected
);
event AssertionEventUintInt(
bool passed,
string message,
string methodName,
uint256 returned,
int256 expected
);
event AssertionEventIntUint(
bool passed,
string message,
string methodName,
int256 returned,
uint256 expected
);
function ok(bool a, string memory message) public returns (bool result) {
result = a;
emit AssertionEvent(result, message, "ok");
}
function equal(uint256 a, uint256 b, string memory message) public returns (bool result) {
result = (a == b);
emit AssertionEventUint(result, message, "equal", a, b);
}
function equal(int256 a, int256 b, string memory message) public returns (bool result) {
result = (a == b);
emit AssertionEventInt(result, message, "equal", a, b);
}
function equal(bool a, bool b, string memory message) public returns (bool result) {
result = (a == b);
emit AssertionEventBool(result, message, "equal", a, b);
}
// TODO: only for certain versions of solc
//function equal(fixed a, fixed b, string message) public returns (bool result) {
// result = (a == b);
// emit AssertionEvent(result, message);
//}
// TODO: only for certain versions of solc
//function equal(ufixed a, ufixed b, string message) public returns (bool result) {
// result = (a == b);
// emit AssertionEvent(result, message);
//}
function equal(address a, address b, string memory message) public returns (bool result) {
result = (a == b);
emit AssertionEventAddress(result, message, "equal", a, b);
}
function equal(bytes32 a, bytes32 b, string memory message) public returns (bool result) {
result = (a == b);
emit AssertionEventBytes32(result, message, "equal", a, b);
}
function equal(string memory a, string memory b, string memory message) public returns (bool result) {
result = (keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)));
emit AssertionEventString(result, message, "equal", a, b);
}
function notEqual(uint256 a, uint256 b, string memory message) public returns (bool result) {
result = (a != b);
emit AssertionEventUint(result, message, "notEqual", a, b);
}
function notEqual(int256 a, int256 b, string memory message) public returns (bool result) {
result = (a != b);
emit AssertionEventInt(result, message, "notEqual", a, b);
}
function notEqual(bool a, bool b, string memory message) public returns (bool result) {
result = (a != b);
emit AssertionEventBool(result, message, "notEqual", a, b);
}
// TODO: only for certain versions of solc
//function notEqual(fixed a, fixed b, string message) public returns (bool result) {
// result = (a != b);
// emit AssertionEvent(result, message);
//}
// TODO: only for certain versions of solc
//function notEqual(ufixed a, ufixed b, string message) public returns (bool result) {
// result = (a != b);
// emit AssertionEvent(result, message);
//}
function notEqual(address a, address b, string memory message) public returns (bool result) {
result = (a != b);
emit AssertionEventAddress(result, message, "notEqual", a, b);
}
function notEqual(bytes32 a, bytes32 b, string memory message) public returns (bool result) {
result = (a != b);
emit AssertionEventBytes32(result, message, "notEqual", a, b);
}
function notEqual(string memory a, string memory b, string memory message) public returns (bool result) {
result = (keccak256(abi.encodePacked(a)) != keccak256(abi.encodePacked(b)));
emit AssertionEventString(result, message, "notEqual", a, b);
}
/*----------------- Greater than --------------------*/
function greaterThan(uint256 a, uint256 b, string memory message) public returns (bool result) {
result = (a > b);
emit AssertionEventUint(result, message, "greaterThan", a, b);
}
function greaterThan(int256 a, int256 b, string memory message) public returns (bool result) {
result = (a > b);
emit AssertionEventInt(result, message, "greaterThan", a, b);
}
// TODO: safely compare between uint and int
function greaterThan(uint256 a, int256 b, string memory message) public returns (bool result) {
if(b < int(0)) {
// int is negative uint "a" always greater
result = true;
} else {
result = (a > uint(b));
}
emit AssertionEventUintInt(result, message, "greaterThan", a, b);
}
function greaterThan(int256 a, uint256 b, string memory message) public returns (bool result) {
if(a < int(0)) {
// int is negative uint "b" always greater
result = false;
} else {
result = (uint(a) > b);
}
emit AssertionEventIntUint(result, message, "greaterThan", a, b);
}
/*----------------- Lesser than --------------------*/
function lesserThan(uint256 a, uint256 b, string memory message) public returns (bool result) {
result = (a < b);
emit AssertionEventUint(result, message, "lesserThan", a, b);
}
function lesserThan(int256 a, int256 b, string memory message) public returns (bool result) {
result = (a < b);
emit AssertionEventInt(result, message, "lesserThan", a, b);
}
// TODO: safely compare between uint and int
function lesserThan(uint256 a, int256 b, string memory message) public returns (bool result) {
if(b < int(0)) {
// int is negative int "b" always lesser
result = false;
} else {
result = (a < uint(b));
}
emit AssertionEventUintInt(result, message, "lesserThan", a, b);
}
function lesserThan(int256 a, uint256 b, string memory message) public returns (bool result) {
if(a < int(0)) {
// int is negative int "a" always lesser
result = true;
} else {
result = (uint(a) < b);
}
emit AssertionEventIntUint(result, message, "lesserThan", a, b);
}
}

@ -39,6 +39,13 @@
"generateIndexHtml": true, "generateIndexHtml": true,
"extractCss": false, "extractCss": false,
"vendorChunk": false "vendorChunk": false
},
"desktop": {
"optimization": false,
"generateIndexHtml": true,
"extractCss": false,
"vendorChunk": false,
"baseHref": "./"
} }
} }
}, },

@ -45,6 +45,14 @@ import { FileDecorator } from './app/plugins/file-decorator'
import { CodeFormat } from './app/plugins/code-format' import { CodeFormat } from './app/plugins/code-format'
import { SolidityUmlGen } from './app/plugins/solidity-umlgen' import { SolidityUmlGen } from './app/plugins/solidity-umlgen'
import { ContractFlattener } from './app/plugins/contractFlattener' import { ContractFlattener } from './app/plugins/contractFlattener'
import { TemplatesPlugin } from './app/plugins/remix-templates'
import { fsPlugin } from './app/plugins/electron/fsPlugin'
import { isoGitPlugin } from './app/plugins/electron/isoGitPlugin'
import { electronConfig } from './app/plugins/electron/electronConfigPlugin'
import { electronTemplates } from './app/plugins/electron/templatesPlugin'
import { xtermPlugin } from './app/plugins/electron/xtermPlugin'
const isElectron = require('is-electron') const isElectron = require('is-electron')
@ -52,13 +60,14 @@ const remixLib = require('@remix-project/remix-lib')
import { QueryParams } from '@remix-project/remix-lib' import { QueryParams } from '@remix-project/remix-lib'
import { SearchPlugin } from './app/tabs/search' import { SearchPlugin } from './app/tabs/search'
import { ElectronProvider } from './app/files/electronProvider'
const Storage = remixLib.Storage const Storage = remixLib.Storage
const RemixDProvider = require('./app/files/remixDProvider') const RemixDProvider = require('./app/files/remixDProvider')
const Config = require('./config') const Config = require('./config')
const FileManager = require('./app/files/fileManager') const FileManager = require('./app/files/fileManager')
const FileProvider = require('./app/files/fileProvider') import FileProvider from "./app/files/fileProvider"
const DGitProvider = require('./app/files/dgitProvider') const DGitProvider = require('./app/files/dgitProvider')
const WorkspaceFileProvider = require('./app/files/workspaceFileProvider') const WorkspaceFileProvider = require('./app/files/workspaceFileProvider')
@ -74,8 +83,11 @@ const Editor = require('./app/editor/editor')
const Terminal = require('./app/panels/terminal') const Terminal = require('./app/panels/terminal')
const { TabProxy } = require('./app/panels/tab-proxy.js') const { TabProxy } = require('./app/panels/tab-proxy.js')
class AppComponent { class AppComponent {
constructor() { constructor() {
this.appManager = new RemixAppManager({}) this.appManager = new RemixAppManager({})
this.queryParams = new QueryParams() this.queryParams = new QueryParams()
this._components = {} this._components = {}
@ -106,6 +118,12 @@ class AppComponent {
name: 'fileproviders/workspace' name: 'fileproviders/workspace'
}) })
this._components.filesProviders.electron = new ElectronProvider(this.appManager)
Registry.getInstance().put({
api: this._components.filesProviders.electron,
name: 'fileproviders/electron'
})
Registry.getInstance().put({ Registry.getInstance().put({
api: this._components.filesProviders, api: this._components.filesProviders,
name: 'fileproviders' name: 'fileproviders'
@ -181,6 +199,9 @@ class AppComponent {
//----- search //----- search
const search = new SearchPlugin() const search = new SearchPlugin()
//---- templates
const templates = new TemplatesPlugin()
//---------------- Solidity UML Generator ------------------------- //---------------- Solidity UML Generator -------------------------
const solidityumlgen = new SolidityUmlGen(appManager) const solidityumlgen = new SolidityUmlGen(appManager)
@ -257,6 +278,7 @@ class AppComponent {
const permissionHandler = new PermissionHandlerPlugin() const permissionHandler = new PermissionHandlerPlugin()
this.engine.register([ this.engine.register([
permissionHandler, permissionHandler,
this.layout, this.layout,
@ -302,9 +324,24 @@ class AppComponent {
search, search,
solidityumlgen, solidityumlgen,
contractFlattener, contractFlattener,
solidityScript solidityScript,
templates
]) ])
//---- fs plugin
if (isElectron()) {
const FSPlugin = new fsPlugin()
this.engine.register([FSPlugin])
const isoGit = new isoGitPlugin()
this.engine.register([isoGit])
const electronConfigPlugin = new electronConfig()
this.engine.register([electronConfigPlugin])
const templatesPlugin = new electronTemplates()
this.engine.register([templatesPlugin])
const xterm = new xtermPlugin()
this.engine.register([xterm])
}
// LAYOUT & SYSTEM VIEWS // LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel() const appPanel = new MainPanel()
Registry.getInstance().put({ api: this.mainview, name: 'mainview' }) Registry.getInstance().put({ api: this.mainview, name: 'mainview' })
@ -418,7 +455,11 @@ class AppComponent {
await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormatter', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormatter', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler'])
await this.appManager.activatePlugin(['settings']) await this.appManager.activatePlugin(['settings'])
await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder']) await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder'])
await this.appManager.activatePlugin(['solidity-script']) await this.appManager.activatePlugin(['solidity-script', 'remix-templates'])
if(isElectron()){
await this.appManager.activatePlugin(['fs', 'isogit', 'electronconfig', 'electronTemplates', 'xterm'])
}
this.appManager.on( this.appManager.on(
'filePanel', 'filePanel',
@ -432,6 +473,7 @@ class AppComponent {
} }
) )
await this.appManager.activatePlugin(['filePanel']) await this.appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation // Set workspace after initial activation
this.appManager.on('editor', 'editorMounted', () => { this.appManager.on('editor', 'editorMounted', () => {

@ -10,10 +10,11 @@ import {
} from 'file-saver' } from 'file-saver'
import http from 'isomorphic-git/http/web' import http from 'isomorphic-git/http/web'
const JSZip = require('jszip') import JSZip from 'jszip'
const path = require('path') import path from 'path'
const FormData = require('form-data') import FormData from 'form-data'
const axios = require('axios') import axios from 'axios'
import isElectron from 'is-electron'
const profile = { const profile = {
name: 'dGitProvider', name: 'dGitProvider',
@ -21,11 +22,17 @@ const profile = {
description: 'Decentralized git provider', description: 'Decentralized git provider',
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
version: '0.0.1', version: '0.0.1',
methods: ['init', 'localStorageUsed', 'addremote', 'delremote', 'remotes', 'fetch', 'clone', 'export', 'import', 'status', 'log', 'commit', 'add', 'remove', 'rm', 'lsfiles', 'readblob', 'resolveref', 'branches', 'branch', 'checkout', 'currentbranch', 'push', 'pin', 'pull', 'pinList', 'unPin', 'setIpfsConfig', 'zip', 'setItem', 'getItem'], methods: ['init', 'localStorageUsed', 'addremote', 'delremote', 'remotes', 'fetch', 'clone', 'export', 'import', 'status', 'log', 'commit', 'add', 'remove', 'reset', 'rm', 'lsfiles', 'readblob', 'resolveref', 'branches', 'branch', 'checkout', 'currentbranch', 'push', 'pin', 'pull', 'pinList', 'unPin', 'setIpfsConfig', 'zip', 'setItem', 'getItem', 'version'],
kind: 'file-system' kind: 'file-system'
} }
class DGitProvider extends Plugin { class DGitProvider extends Plugin {
constructor () { ipfsconfig: { host: string; port: number; protocol: string; ipfsurl: string }
globalIPFSConfig: { host: string; port: number; protocol: string; ipfsurl: string }
remixIPFS: { host: string; port: number; protocol: string; ipfsurl: string }
ipfsSources: any[]
ipfs: any
filesToSend: any[]
constructor() {
super(profile) super(profile)
this.ipfsconfig = { this.ipfsconfig = {
host: 'jqgt.remixproject.org', host: 'jqgt.remixproject.org',
@ -48,7 +55,15 @@ class DGitProvider extends Plugin {
this.ipfsSources = [this.remixIPFS, this.globalIPFSConfig, this.ipfsconfig] this.ipfsSources = [this.remixIPFS, this.globalIPFSConfig, this.ipfsconfig]
} }
async getGitConfig () { async getGitConfig() {
if (isElectron()) {
return {
fs: window.remixFileSystem,
dir: '/'
}
}
const workspace = await this.call('filePanel', 'getCurrentWorkspace') const workspace = await this.call('filePanel', 'getCurrentWorkspace')
if (!workspace) return if (!workspace) return
@ -58,7 +73,7 @@ class DGitProvider extends Plugin {
} }
} }
async parseInput (input) { async parseInput(input) {
return { return {
corsProxy: 'https://corsproxy.remixproject.org/', corsProxy: 'https://corsproxy.remixproject.org/',
http, http,
@ -73,7 +88,15 @@ class DGitProvider extends Plugin {
} }
} }
async init (input) { async init(input?) {
if (isElectron()) {
await this.call('isogit', 'init', {
defaultBranch: (input && input.branch) || 'main'
})
this.emit('init')
return
}
await git.init({ await git.init({
...await this.getGitConfig(), ...await this.getGitConfig(),
defaultBranch: (input && input.branch) || 'main' defaultBranch: (input && input.branch) || 'main'
@ -81,34 +104,84 @@ class DGitProvider extends Plugin {
this.emit('init') this.emit('init')
} }
async status (cmd) { async version() {
if (isElectron()) {
return await this.call('isogit', 'version')
}
const version = 'built-in'
return version
}
async status(cmd) {
if (isElectron()) {
const status = await this.call('isogit', 'status', cmd)
return status
}
const status = await git.statusMatrix({ const status = await git.statusMatrix({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd
}) })
return status return status
} }
async add (cmd) { async add(cmd) {
if (isElectron()) {
await this.call('isogit', 'add', cmd)
} else {
await git.add({ await git.add({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd
}) })
}
this.emit('add') this.emit('add')
} }
async rm (cmd) { async rm(cmd) {
if (isElectron()) {
await this.call('isogit', 'rm', cmd)
} else {
await git.remove({ await git.remove({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd
}) })
this.emit('rm')
}
}
async reset(cmd) {
if (isElectron()) {
await this.call('isogit', 'reset', cmd)
} else {
await git.resetIndex({
...await this.getGitConfig(),
...cmd
})
this.emit('rm')
}
} }
async checkout (cmd, refresh = true) { async checkout(cmd, refresh = true) {
if (isElectron()) {
await this.call('isogit', 'checkout', cmd)
} else {
await git.checkout({ await git.checkout({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd
}) })
}
if (refresh) { if (refresh) {
setTimeout(async () => { setTimeout(async () => {
await this.call('fileManager', 'refresh') await this.call('fileManager', 'refresh')
@ -117,15 +190,31 @@ class DGitProvider extends Plugin {
this.emit('checkout') this.emit('checkout')
} }
async log (cmd) { async log(cmd) {
if (isElectron()) {
const status = await this.call('isogit', 'log', {
...cmd,
depth: 10
})
return status
}
const status = await git.log({ const status = await git.log({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd,
depth: 10
}) })
return status return status
} }
async remotes (config) { async remotes(config) {
if (isElectron()) {
return await this.call('isogit', 'remotes', config)
}
let remotes = [] let remotes = []
try { try {
remotes = await git.listRemotes({ ...config ? config : await this.getGitConfig() }) remotes = await git.listRemotes({ ...config ? config : await this.getGitConfig() })
@ -135,11 +224,17 @@ class DGitProvider extends Plugin {
return remotes return remotes
} }
async branch (cmd, refresh = true) { async branch(cmd, refresh = true) {
const status = await git.branch({
let status
if (isElectron()) {
status = await this.call('isogit', 'branch', cmd)
} else {
status = await git.branch({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd
}) })
}
if (refresh) { if (refresh) {
setTimeout(async () => { setTimeout(async () => {
await this.call('fileManager', 'refresh') await this.call('fileManager', 'refresh')
@ -149,20 +244,32 @@ class DGitProvider extends Plugin {
return status return status
} }
async currentbranch (config) { async currentbranch(config) {
try{
if (isElectron()) {
return await this.call('isogit', 'currentbranch')
}
try {
const defaultConfig = await this.getGitConfig() const defaultConfig = await this.getGitConfig()
const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig
const name = await git.currentBranch(cmd) const name = await git.currentBranch(cmd)
return name return name
}catch(e){ } catch (e) {
return '' return ''
} }
} }
async branches (config) { async branches(config) {
try{
if (isElectron()) {
return await this.call('isogit', 'branches')
}
try {
const defaultConfig = await this.getGitConfig() const defaultConfig = await this.getGitConfig()
const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig
const remotes = await this.remotes(config) const remotes = await this.remotes(config)
@ -174,12 +281,24 @@ class DGitProvider extends Plugin {
branches = [...branches, ...remotebranches] branches = [...branches, ...remotebranches]
} }
return branches return branches
}catch(e){ } catch (e) {
return [] return []
} }
} }
async commit (cmd) { async commit(cmd) {
if (isElectron()) {
try {
await this.call('isogit', 'init')
const sha = await this.call('isogit', 'commit', cmd)
this.emit('commit')
return sha
} catch (e) {
throw new Error(e)
}
} else {
await this.init() await this.init()
try { try {
const sha = await git.commit({ const sha = await git.commit({
@ -192,8 +311,14 @@ class DGitProvider extends Plugin {
throw new Error(e) throw new Error(e)
} }
} }
}
async lsfiles(cmd) {
if (isElectron()) {
return await this.call('isogit', 'lsfiles', cmd)
}
async lsfiles (cmd) {
const filesInStaging = await git.listFiles({ const filesInStaging = await git.listFiles({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd
@ -201,7 +326,12 @@ class DGitProvider extends Plugin {
return filesInStaging return filesInStaging
} }
async resolveref (cmd) { async resolveref(cmd) {
if (isElectron()) {
return await this.call('isogit', 'resolveref', cmd)
}
const oid = await git.resolveRef({ const oid = await git.resolveRef({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd
@ -209,22 +339,27 @@ class DGitProvider extends Plugin {
return oid return oid
} }
async readblob (cmd) { async readblob(cmd) {
if (isElectron()) {
const readBlobResult = await this.call('isogit', 'readblob', cmd)
return readBlobResult
}
const readBlobResult = await git.readBlob({ const readBlobResult = await git.readBlob({
...await this.getGitConfig(), ...await this.getGitConfig(),
...cmd ...cmd
}) })
return readBlobResult return readBlobResult
} }
async setIpfsConfig (config) { async setIpfsConfig(config) {
this.ipfsconfig = config this.ipfsconfig = config
return new Promise((resolve) => { return new Promise((resolve) => {
resolve(this.checkIpfsConfig()) resolve(this.checkIpfsConfig())
}) })
} }
async checkIpfsConfig (config) { async checkIpfsConfig(config?) {
this.ipfs = IpfsHttpClient(config || this.ipfsconfig) this.ipfs = IpfsHttpClient(config || this.ipfsconfig)
try { try {
await this.ipfs.config.getAll() await this.ipfs.config.getAll()
@ -234,22 +369,46 @@ class DGitProvider extends Plugin {
} }
} }
async addremote (input) { async addremote(input) {
if (isElectron()) {
await this.call('isogit', 'addremote', { url: input.url, remote: input.remote })
return
}
await git.addRemote({ ...await this.getGitConfig(), url: input.url, remote: input.remote }) await git.addRemote({ ...await this.getGitConfig(), url: input.url, remote: input.remote })
} }
async delremote (input) { async delremote(input) {
if (isElectron()) {
await this.call('isogit', 'delremote', { remote: input.remote })
return
}
await git.deleteRemote({ ...await this.getGitConfig(), remote: input.remote }) await git.deleteRemote({ ...await this.getGitConfig(), remote: input.remote })
} }
async localStorageUsed () { async localStorageUsed() {
return this.calculateLocalStorage() return this.calculateLocalStorage()
} }
async clone (input, workspaceName, workspaceExists = false) { async clone(input, workspaceName, workspaceExists = false) {
if (isElectron()) {
const folder = await this.call('fs', 'selectFolder')
if (!folder) return false
const cmd = {
url: input.url,
singleBranch: input.singleBranch,
ref: input.branch,
depth: input.depth || 10,
dir: folder,
input
}
const result = await this.call('isogit', 'clone', cmd)
this.call('fs', 'openWindow', folder)
return result
} else {
const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.') const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.')
if (!permission) return false if (!permission) return false
if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.') if (parseFloat(this.calculateLocalStorage()) > 10000) throw new Error('The local storage of the browser is full.')
if (!workspaceExists) await this.call('filePanel', 'createWorkspace', workspaceName || `workspace_${Date.now()}`, true) if (!workspaceExists) await this.call('filePanel', 'createWorkspace', workspaceName || `workspace_${Date.now()}`, true)
const cmd = { const cmd = {
url: input.url, url: input.url,
@ -269,8 +428,9 @@ class DGitProvider extends Plugin {
this.emit('clone') this.emit('clone')
return result return result
} }
}
async push (input) { async push(input) {
const cmd = { const cmd = {
force: input.force, force: input.force,
ref: input.ref, ref: input.ref,
@ -280,13 +440,25 @@ class DGitProvider extends Plugin {
name: input.name, name: input.name,
email: input.email email: input.email
}, },
input,
}
if (isElectron()) {
return await this.call('isogit', 'push', cmd)
} else {
const cmd2 = {
...cmd,
...await this.parseInput(input), ...await this.parseInput(input),
...await this.getGitConfig()
} }
return await git.push(cmd) return await git.push({
...await this.getGitConfig(),
...cmd2
})
}
} }
async pull (input) { async pull(input) {
const cmd = { const cmd = {
ref: input.ref, ref: input.ref,
remoteRef: input.remoteRef, remoteRef: input.remoteRef,
@ -295,17 +467,29 @@ class DGitProvider extends Plugin {
email: input.email email: input.email
}, },
remote: input.remote, remote: input.remote,
input,
}
let result
if (isElectron()) {
result = await this.call('isogit', 'pull', cmd)
}
else {
const cmd2 = {
...cmd,
...await this.parseInput(input), ...await this.parseInput(input),
...await this.getGitConfig()
} }
const result = await git.pull(cmd) result = await git.pull({
...await this.getGitConfig(),
...cmd2
})
}
setTimeout(async () => { setTimeout(async () => {
await this.call('fileManager', 'refresh') await this.call('fileManager', 'refresh')
}, 1000) }, 1000)
return result return result
} }
async fetch (input) { async fetch(input) {
const cmd = { const cmd = {
ref: input.ref, ref: input.ref,
remoteRef: input.remoteRef, remoteRef: input.remoteRef,
@ -314,17 +498,28 @@ class DGitProvider extends Plugin {
email: input.email email: input.email
}, },
remote: input.remote, remote: input.remote,
input
}
let result
if (isElectron()) {
result = await this.call('isogit', 'fetch', cmd)
} else {
const cmd2 = {
...cmd,
...await this.parseInput(input), ...await this.parseInput(input),
...await this.getGitConfig()
} }
const result = await git.fetch(cmd) result = await git.fetch({
...await this.getGitConfig(),
...cmd2
})
}
setTimeout(async () => { setTimeout(async () => {
await this.call('fileManager', 'refresh') await this.call('fileManager', 'refresh')
}, 1000) }, 1000)
return result return result
} }
async export (config) { async export(config) {
if (!this.checkIpfsConfig(config)) return false if (!this.checkIpfsConfig(config)) return false
const workspace = await this.call('filePanel', 'getCurrentWorkspace') const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const files = await this.getDirectory('/') const files = await this.getDirectory('/')
@ -344,7 +539,7 @@ class DGitProvider extends Plugin {
return r.cid.string return r.cid.string
} }
async pin (pinataApiKey, pinataSecretApiKey) { async pin(pinataApiKey, pinataSecretApiKey) {
const workspace = await this.call('filePanel', 'getCurrentWorkspace') const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const files = await this.getDirectory('/') const files = await this.getDirectory('/')
this.filesToSend = [] this.filesToSend = []
@ -397,21 +592,21 @@ class DGitProvider extends Plugin {
.post(url, data, { .post(url, data, {
maxBodyLength: 'Infinity', maxBodyLength: 'Infinity',
headers: { headers: {
'Content-Type': `multipart/form-data; boundary=${data._boundary}`, 'Content-Type': `multipart/form-data; boundary=${(data as any)._boundary}`,
pinata_api_key: pinataApiKey, pinata_api_key: pinataApiKey,
pinata_secret_api_key: pinataSecretApiKey pinata_secret_api_key: pinataSecretApiKey
} }
}).catch((e) => { } as any).catch((e) => {
console.log(e) console.log(e)
}) })
// also commit to remix IPFS for availability after pinning to Pinata // also commit to remix IPFS for availability after pinning to Pinata
return await this.export(this.remixIPFS) || result.data.IpfsHash return await this.export(this.remixIPFS) || (result as any).data.IpfsHash
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} }
} }
async pinList (pinataApiKey, pinataSecretApiKey) { async pinList(pinataApiKey, pinataSecretApiKey) {
const url = 'https://api.pinata.cloud/data/pinList?status=pinned' const url = 'https://api.pinata.cloud/data/pinList?status=pinned'
try { try {
const result = await axios const result = await axios
@ -421,16 +616,16 @@ class DGitProvider extends Plugin {
pinata_api_key: pinataApiKey, pinata_api_key: pinataApiKey,
pinata_secret_api_key: pinataSecretApiKey pinata_secret_api_key: pinataSecretApiKey
} }
}).catch((e) => { } as any).catch((e) => {
console.log('Pinata unreachable') console.log('Pinata unreachable')
}) })
return result.data return (result as any).data
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} }
} }
async unPin (pinataApiKey, pinataSecretApiKey, hashToUnpin) { async unPin(pinataApiKey, pinataSecretApiKey, hashToUnpin) {
const url = `https://api.pinata.cloud/pinning/unpin/${hashToUnpin}` const url = `https://api.pinata.cloud/pinning/unpin/${hashToUnpin}`
try { try {
await axios await axios
@ -446,7 +641,7 @@ class DGitProvider extends Plugin {
} }
} }
async importIPFSFiles (config, cid, workspace) { async importIPFSFiles(config, cid, workspace) {
const ipfs = IpfsHttpClient(config) const ipfs = IpfsHttpClient(config)
let result = false let result = false
try { try {
@ -475,9 +670,9 @@ class DGitProvider extends Plugin {
return result return result
} }
calculateLocalStorage () { calculateLocalStorage() {
var _lsTotal = 0 let _lsTotal = 0
var _xLen; var _x let _xLen; let _x
for (_x in localStorage) { for (_x in localStorage) {
// eslint-disable-next-line no-prototype-builtins // eslint-disable-next-line no-prototype-builtins
if (!localStorage.hasOwnProperty(_x)) { if (!localStorage.hasOwnProperty(_x)) {
@ -489,10 +684,10 @@ class DGitProvider extends Plugin {
return (_lsTotal / 1024).toFixed(2) return (_lsTotal / 1024).toFixed(2)
} }
async import (cmd) { async import(cmd) {
const permission = await this.askUserPermission('import', 'Import multiple files into your workspaces.') const permission = await this.askUserPermission('import', 'Import multiple files into your workspaces.')
if (!permission) return false if (!permission) return false
if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.') if (parseFloat(this.calculateLocalStorage()) > 10000) throw new Error('The local storage of the browser is full.')
const cid = cmd.cid const cid = cmd.cid
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true) await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true)
const workspace = await this.call('filePanel', 'getCurrentWorkspace') const workspace = await this.call('filePanel', 'getCurrentWorkspace')
@ -508,13 +703,13 @@ class DGitProvider extends Plugin {
if (!result) throw new Error(`Cannot pull files from IPFS at ${cid}`) if (!result) throw new Error(`Cannot pull files from IPFS at ${cid}`)
} }
async getItem (name) { async getItem(name) {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
return window.localStorage.getItem(name) return window.localStorage.getItem(name)
} }
} }
async setItem (name, content) { async setItem(name, content) {
try { try {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.localStorage.setItem(name, content) window.localStorage.setItem(name, content)
@ -526,7 +721,7 @@ class DGitProvider extends Plugin {
return true return true
} }
async zip () { async zip() {
const zip = new JSZip() const zip = new JSZip()
const workspace = await this.call('filePanel', 'getCurrentWorkspace') const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const files = await this.getDirectory('/') const files = await this.getDirectory('/')
@ -543,7 +738,7 @@ class DGitProvider extends Plugin {
}) })
} }
async createDirectories (strdirectories) { async createDirectories(strdirectories) {
const ignore = ['.', '/.', ''] const ignore = ['.', '/.', '']
if (ignore.indexOf(strdirectories) > -1) return false if (ignore.indexOf(strdirectories) > -1) return false
const directories = strdirectories.split('/') const directories = strdirectories.split('/')
@ -561,7 +756,7 @@ class DGitProvider extends Plugin {
} }
} }
async getDirectory (dir) { async getDirectory(dir) {
let result = [] let result = []
const files = await this.call('fileManager', 'readdir', dir) const files = await this.call('fileManager', 'readdir', dir)
const fileArray = normalize(files) const fileArray = normalize(files)
@ -585,7 +780,7 @@ class DGitProvider extends Plugin {
} }
const addSlash = (file) => { const addSlash = (file) => {
if (!file.startsWith('/'))file = '/' + file if (!file.startsWith('/')) file = '/' + file
return file return file
} }

@ -0,0 +1,84 @@
import FileProvider from "./fileProvider"
declare global {
interface Window {
remixFileSystem: any
}
}
export class ElectronProvider extends FileProvider {
_appManager: any
constructor(appManager) {
super('')
this._appManager = appManager
}
async init() {
this._appManager.on('fs', 'change', (event, path) => {
switch (event) {
case 'add':
this.event.emit('fileAdded', path)
break
case 'unlink':
this.event.emit('fileRemoved', path)
break
case 'change':
this.get(path, (_error, content) => {
this.event.emit('fileExternallyChanged', path, content, false)
})
break
case 'rename':
this.event.emit('fileRenamed', path)
break
case 'addDir':
this.event.emit('folderAdded', path)
break
case 'unlinkDir':
this.event.emit('fileRemoved', path)
}
})
}
// isDirectory is already included
// this is a more efficient version of the default implementation
async resolveDirectory(path, cb) {
path = this.removePrefix(path)
if (path.indexOf('/') !== 0) path = '/' + path
try {
const files = await window.remixFileSystem.readdir(path)
const ret = {}
if (files) {
for (const element of files) {
path = path.replace(/^\/|\/$/g, '') // remove first and last slash
const file = element.file.replace(/^\/|\/$/g, '') // remove first and last slash
const absPath = (path === '/' ? '' : path) + '/' + file
ret[absPath.indexOf('/') === 0 ? absPath.substr(1, absPath.length) : absPath] = { isDirectory: element.isDirectory }
// ^ ret does not accept path starting with '/'
}
}
//console.log(ret, 'ret resolveDirectory ELECTRON')
if (cb) cb(null, ret)
return ret
} catch (error) {
if (cb) cb(error, null)
}
}
/**
* Removes the folder recursively
* @param {*} path is the folder to be removed
*/
async remove(path: string) {
console.log('remove', path)
try {
await window.remixFileSystem.rmdir(path)
return true
} catch (error) {
console.log(error)
return false
}
}
}

@ -9,6 +9,8 @@ import { fileChangedToastMsg, recursivePasteToastMsg, storageFullMessage } from
import helper from '../../lib/helper.js' import helper from '../../lib/helper.js'
import { RemixAppManager } from '../../remixAppManager' import { RemixAppManager } from '../../remixAppManager'
import isElectron from 'is-electron'
/* /*
attach to files event (removed renamed) attach to files event (removed renamed)
trigger: currentFileChanged trigger: currentFileChanged
@ -22,7 +24,7 @@ const profile = {
permission: true, permission: true,
version: packageJson.version, version: packageJson.version,
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir',
'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'selectFolder', 'setFile', 'switchFile', 'refresh',
'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles', 'isGitRepo'], 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles', 'isGitRepo'],
kind: 'file-system' kind: 'file-system'
} }
@ -153,9 +155,13 @@ class FileManager extends Plugin {
refresh() { refresh() {
const provider = this.fileProviderOf('/') const provider = this.fileProviderOf('/')
// emit rootFolderChanged so that File Explorer reloads the file tree // emit rootFolderChanged so that File Explorer reloads the file tree
if(isElectron()){
provider.event.emit('refresh')
}else{
provider.event.emit('rootFolderChanged', provider.workspace || '/') provider.event.emit('rootFolderChanged', provider.workspace || '/')
this.emit('rootFolderChanged', provider.workspace || '/') this.emit('rootFolderChanged', provider.workspace || '/')
} }
}
/** /**
* Verify if the path provided is a file * Verify if the path provided is a file
@ -189,8 +195,8 @@ class FileManager extends Plugin {
path = this.normalize(path) path = this.normalize(path)
path = this.limitPluginScope(path) path = this.limitPluginScope(path)
path = this.getPathFromUrl(path).file path = this.getPathFromUrl(path).file
await this._handleExists(path, `Cannot open file ${path}`) //await this._handleExists(path, `Cannot open file ${path}`)
await this._handleIsFile(path, `Cannot open file ${path}`) //await this._handleIsFile(path, `Cannot open file ${path}`)
await this.openFile(path) await this.openFile(path)
} }
@ -408,7 +414,6 @@ class FileManager extends Plugin {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const provider = this.fileProviderOf(path) const provider = this.fileProviderOf(path)
provider.resolveDirectory(path, (error, filesProvider) => { provider.resolveDirectory(path, (error, filesProvider) => {
if (error) reject(error) if (error) reject(error)
resolve(filesProvider) resolve(filesProvider)
@ -442,7 +447,8 @@ class FileManager extends Plugin {
browserExplorer: this._components.registry.get('fileproviders/browser').api, browserExplorer: this._components.registry.get('fileproviders/browser').api,
localhostExplorer: this._components.registry.get('fileproviders/localhost').api, localhostExplorer: this._components.registry.get('fileproviders/localhost').api,
workspaceExplorer: this._components.registry.get('fileproviders/workspace').api, workspaceExplorer: this._components.registry.get('fileproviders/workspace').api,
filesProviders: this._components.registry.get('fileproviders').api filesProviders: this._components.registry.get('fileproviders').api,
electronExplorer: this._components.registry.get('fileproviders/electron').api,
} }
this._deps.config.set('currentFile', '') // make sure we remove the current file from the previous session this._deps.config.set('currentFile', '') // make sure we remove the current file from the previous session
@ -460,6 +466,11 @@ class FileManager extends Plugin {
this._deps.workspaceExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) }) this._deps.workspaceExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.workspaceExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) }) this._deps.workspaceExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) })
this._deps.electronExplorer.event.on('fileChanged', (path) => { this.fileChangedEvent(path) })
this._deps.electronExplorer.event.on('fileRenamed', (oldName, newName, isFolder) => { this.fileRenamedEvent(oldName, newName, isFolder) })
this._deps.electronExplorer.event.on('fileRemoved', (path) => { this.fileRemovedEvent(path) })
this._deps.electronExplorer.event.on('fileAdded', (path) => { this.fileAddedEvent(path) })
this.getCurrentFile = this.file this.getCurrentFile = this.file
this.getFile = this.readFile this.getFile = this.readFile
this.getFolder = this.readdir this.getFolder = this.readdir
@ -721,8 +732,9 @@ class FileManager extends Plugin {
if (file.startsWith('localhost') || this.mode === 'localhost') { if (file.startsWith('localhost') || this.mode === 'localhost') {
return this._deps.filesProviders.localhost return this._deps.filesProviders.localhost
} }
if (file.startsWith('browser')) {
return this._deps.filesProviders.browser if(isElectron()){
return this._deps.filesProviders.electron
} }
return this._deps.filesProviders.workspace return this._deps.filesProviders.workspace
} }
@ -846,6 +858,10 @@ class FileManager extends Plugin {
} }
currentWorkspace() { currentWorkspace() {
if(isElectron()){
return ''
}
if (this.mode !== 'localhost') { if (this.mode !== 'localhost') {
const file = this.currentFile() || '' const file = this.currentFile() || ''
const provider = this.fileProviderOf(file) const provider = this.fileProviderOf(file)

@ -1,12 +1,17 @@
'use strict' 'use strict'
import { CompilerImports } from '@remix-project/core-plugin' import { CompilerImports } from '@remix-project/core-plugin'
const EventManager = require('events') import EventManager from 'events'
const remixLib = require('@remix-project/remix-lib') import { Storage } from '@remix-project/remix-lib'
const pathModule = require('path') import pathModule from 'path'
const Storage = remixLib.Storage
class FileProvider {
export default class FileProvider {
event: any
type: any
providerExternalsStorage: any
externalFolders: string[]
reverseKey: string
constructor (name) { constructor (name) {
this.event = new EventManager() this.event = new EventManager()
this.type = name this.type = name
@ -79,7 +84,7 @@ class FileProvider {
async _exists (path) { async _exists (path) {
path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here
var unprefixedpath = this.removePrefix(path) const unprefixedpath = this.removePrefix(path)
return path === this.type ? true : await window.remixFileSystem.exists(unprefixedpath) return path === this.type ? true : await window.remixFileSystem.exists(unprefixedpath)
} }
@ -90,7 +95,7 @@ class FileProvider {
async get (path, cb) { async get (path, cb) {
cb = cb || function () { /* do nothing. */ } cb = cb || function () { /* do nothing. */ }
path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here
var unprefixedpath = this.removePrefix(path) const unprefixedpath = this.removePrefix(path)
try { try {
const content = await window.remixFileSystem.readFile(unprefixedpath, 'utf8') const content = await window.remixFileSystem.readFile(unprefixedpath, 'utf8')
if (cb) cb(null, content) if (cb) cb(null, content)
@ -103,13 +108,13 @@ class FileProvider {
async set (path, content, cb) { async set (path, content, cb) {
cb = cb || function () { /* do nothing. */ } cb = cb || function () { /* do nothing. */ }
var unprefixedpath = this.removePrefix(path) const unprefixedpath = this.removePrefix(path)
const exists = await window.remixFileSystem.exists(unprefixedpath) const exists = await window.remixFileSystem.exists(unprefixedpath)
if (exists && await window.remixFileSystem.readFile(unprefixedpath, 'utf8') === content) { if (exists && await window.remixFileSystem.readFile(unprefixedpath, 'utf8') === content) {
if (cb) cb() if (cb) cb()
return null return null
} }
await this.createDir(path.substr(0, path.lastIndexOf('/'))) await this.createDir(path.substr(0, path.lastIndexOf('/')), null)
try { try {
await window.remixFileSystem.writeFile(unprefixedpath, content, 'utf8') await window.remixFileSystem.writeFile(unprefixedpath, content, 'utf8')
} catch (e) { } catch (e) {
@ -152,7 +157,7 @@ class FileProvider {
// this will not add a folder as readonly but keep the original url to be able to restore it later // this will not add a folder as readonly but keep the original url to be able to restore it later
async addExternal (path, content, url) { async addExternal (path, content, url) {
if (url) this.addNormalizedName(path, url) if (url) this.addNormalizedName(path, url)
return await this.set(path, content) return await this.set(path, content, null)
} }
isReadOnly (path) { isReadOnly (path) {
@ -161,7 +166,8 @@ class FileProvider {
async isDirectory (path) { async isDirectory (path) {
const unprefixedpath = this.removePrefix(path) const unprefixedpath = this.removePrefix(path)
return path === this.type ? true : (await window.remixFileSystem.stat(unprefixedpath)).isDirectory() const isDirectory = path === this.type ? true : (await window.remixFileSystem.stat(unprefixedpath)).isDirectory()
return isDirectory
} }
async isFile (path) { async isFile (path) {
@ -174,7 +180,7 @@ class FileProvider {
* Removes the folder recursively * Removes the folder recursively
* @param {*} path is the folder to be removed * @param {*} path is the folder to be removed
*/ */
async remove (path) { async remove (path: string) {
path = this.removePrefix(path) path = this.removePrefix(path)
if (await window.remixFileSystem.exists(path)) { if (await window.remixFileSystem.exists(path)) {
const stat = await window.remixFileSystem.stat(path) const stat = await window.remixFileSystem.stat(path)
@ -225,7 +231,7 @@ class FileProvider {
visitFolder({ path }) visitFolder({ path })
if (items.length !== 0) { if (items.length !== 0) {
for (const item of items) { for (const item of items) {
const file = {} const file: any = {}
const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}` const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}`
if ((await window.remixFileSystem.stat(curPath)).isDirectory()) { if ((await window.remixFileSystem.stat(curPath)).isDirectory()) {
file.children = await this._copyFolderToJsonInternal(curPath, visitFile, visitFolder) file.children = await this._copyFolderToJsonInternal(curPath, visitFile, visitFolder)
@ -266,8 +272,8 @@ class FileProvider {
} }
async rename (oldPath, newPath, isFolder) { async rename (oldPath, newPath, isFolder) {
var unprefixedoldPath = this.removePrefix(oldPath) const unprefixedoldPath = this.removePrefix(oldPath)
var unprefixednewPath = this.removePrefix(newPath) const unprefixednewPath = this.removePrefix(newPath)
if (await this._exists(unprefixedoldPath)) { if (await this._exists(unprefixedoldPath)) {
await window.remixFileSystem.rename(unprefixedoldPath, unprefixednewPath) await window.remixFileSystem.rename(unprefixedoldPath, unprefixednewPath)
this.event.emit('fileRenamed', this.event.emit('fileRenamed',
@ -321,4 +327,3 @@ class FileProvider {
} }
} }
module.exports = FileProvider

@ -1,5 +1,5 @@
'use strict' 'use strict'
const FileProvider = require('./fileProvider') import FileProvider from "./fileProvider"
module.exports = class RemixDProvider extends FileProvider { module.exports = class RemixDProvider extends FileProvider {
constructor (appManager) { constructor (appManager) {

@ -1,7 +1,7 @@
'use strict' 'use strict'
const EventManager = require('events') const EventManager = require('events')
const FileProvider = require('./fileProvider') import FileProvider from "./fileProvider"
class WorkspaceFileProvider extends FileProvider { class WorkspaceFileProvider extends FileProvider {
constructor () { constructor () {

@ -30,7 +30,7 @@ const { SlitherHandle } = require('../files/slither-handle.js')
const profile = { const profile = {
name: 'filePanel', name: 'filePanel',
displayName: 'File explorer', displayName: 'File explorer',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getAvailableWorkspaceName', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'], methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getAvailableWorkspaceName', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace', 'loadTemplate', 'clone'],
events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'], events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
description: 'Remix IDE file explorer', description: 'Remix IDE file explorer',

@ -6,14 +6,16 @@ import * as packageJson from '../../../../../package.json'
import Registry from '../state/registry' import Registry from '../state/registry'
import { PluginViewWrapper } from '@remix-ui/helper' import { PluginViewWrapper } from '@remix-ui/helper'
import vm from 'vm' import vm from 'vm'
import isElectron from 'is-electron'
const EventManager = require('../../lib/events') const EventManager = require('../../lib/events')
import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line
import { RemixUiXterminals } from '@remix-ui/xterm'
const KONSOLES = [] const KONSOLES = []
function register (api) { KONSOLES.push(api) } function register(api) { KONSOLES.push(api) }
const profile = { const profile = {
displayName: 'Terminal', displayName: 'Terminal',
@ -25,7 +27,7 @@ const profile = {
} }
class Terminal extends Plugin { class Terminal extends Plugin {
constructor (opts, api) { constructor(opts, api) {
super(profile) super(profile)
this.fileImport = new CompilerImports() this.fileImport = new CompilerImports()
this.event = new EventManager() this.event = new EventManager()
@ -89,18 +91,18 @@ class Terminal extends Plugin {
this.renderComponent() this.renderComponent()
} }
onDeactivation () { onDeactivation() {
this.off('scriptRunner', 'log') this.off('scriptRunner', 'log')
this.off('scriptRunner', 'info') this.off('scriptRunner', 'info')
this.off('scriptRunner', 'warn') this.off('scriptRunner', 'warn')
this.off('scriptRunner', 'error') this.off('scriptRunner', 'error')
} }
logHtml (html) { logHtml(html) {
this.terminalApi.logHtml(html) this.terminalApi.logHtml(html)
} }
log (message, type) { log(message, type) {
this.terminalApi.log(message, type) this.terminalApi.log(message, type)
} }
@ -108,18 +110,20 @@ class Terminal extends Plugin {
this.dispatch = dispatch this.dispatch = dispatch
} }
render () { render() {
return <div id='terminal-view' className='panel' data-id='terminalContainer-view'><PluginViewWrapper plugin={this}/></div> return <div id='terminal-view' className='panel' data-id='terminalContainer-view'><PluginViewWrapper plugin={this} /></div>
} }
updateComponent(state) { updateComponent(state) {
return <RemixUiTerminal return isElectron() ? <RemixUiXterminals onReady={state.onReady} plugin={state.plugin}>
</RemixUiXterminals>
: <RemixUiTerminal
plugin={state.plugin} plugin={state.plugin}
onReady={state.onReady} onReady={state.onReady}
/> />
} }
renderComponent () { renderComponent() {
const onReady = (api) => { this.terminalApi = api } const onReady = (api) => { this.terminalApi = api }
this.dispatch({ this.dispatch({
plugin: this, plugin: this,
@ -127,7 +131,7 @@ class Terminal extends Plugin {
}) })
} }
scroll2bottom () { scroll2bottom() {
setTimeout(function () { setTimeout(function () {
// do nothing. // do nothing.
}, 0) }, 0)

@ -0,0 +1,19 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class electronConfig extends ElectronPlugin {
constructor() {
super({
displayName: 'electronconfig',
name: 'electronconfig',
description: 'electronconfig',
})
this.methods = []
}
onActivation(): void {
}
}

@ -0,0 +1,143 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
let workingDir = null
const fixPath = (path: string) => {
return path
}
export class fsPlugin extends ElectronPlugin {
public fs: any
public fsSync: any
constructor() {
super({
displayName: 'fs',
name: 'fs',
description: 'fs',
})
this.methods = ['readdir', 'readFile', 'writeFile', 'mkdir', 'rmdir', 'unlink', 'rename', 'stat', 'lstat', 'exists', 'setWorkingDir', 'getRecentFolders', 'glob', 'openWindow']
// List of commands all filesystems are expected to provide. `rm` is not
// included since it may not exist and must be handled as a special case
const commands = [
'readFile',
'writeFile',
'mkdir',
'rmdir',
'unlink',
'stat',
'lstat',
'readdir',
'readlink',
'symlink',
]
this.fs = {
exists: async (path: string) => {
path = fixPath(path)
const exists = await this.call('fs', 'exists', path)
return exists
},
rmdir: async (path: string) => {
path = fixPath(path)
return await this.call('fs', 'rmdir', path)
},
readdir: async (path: string) => {
path = fixPath(path)
const files = await this.call('fs', 'readdir', path)
return files
},
glob: async (path: string, pattern: string, options?: any) => {
path = fixPath(path)
const files = await this.call('fs', 'glob', path, pattern, options)
return files
},
unlink: async (path: string) => {
path = fixPath(path)
return await this.call('fs', 'unlink', path)
},
mkdir: async (path: string) => {
path = fixPath(path)
return await this.call('fs', 'mkdir', path)
},
readFile: async (path: string, options) => {
try {
path = fixPath(path)
const file = await this.call('fs', 'readFile', path, options)
return file
} catch (e) {
return undefined
}
}
,
rename: async (from: string, to: string) => {
return await this.call('fs', 'rename', from, to)
},
writeFile: async (path: string, content: string, options: any) => {
path = fixPath(path)
return await this.call('fs', 'writeFile', path, content, options)
}
,
stat: async (path: string) => {
try {
path = fixPath(path)
const stat = await this.call('fs', 'stat', path)
if(!stat) return undefined
stat.isDirectory = () => stat.isDirectoryValue
stat.isFile = () => !stat.isDirectoryValue
return stat
} catch (e) {
return undefined
}
},
lstat: async (path: string) => {
try {
path = fixPath(path)
const stat = await this.call('fs', 'lstat', path)
if(!stat) return undefined
stat.isDirectory = () => stat.isDirectoryValue
stat.isFile = () => !stat.isDirectoryValue
return stat
} catch (e) {
return undefined
}
},
readlink: async (path: string) => {
path = fixPath(path)
return await this.call('fs', 'readlink', path)
},
symlink: async (target: string, path: string) => {
path = fixPath(path)
return await this.call('fs', 'symlink', target, path)
}
}
}
async onActivation() {
(window as any).remixFileSystem = this.fs;
this.on('fs', 'workingDirChanged', async (path: string) => {
workingDir = path
await this.call('fileManager', 'refresh')
})
}
}

@ -0,0 +1,29 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class isoGitPlugin extends ElectronPlugin {
constructor() {
super({
displayName: 'isogit',
name: 'isogit',
description: 'isogit',
})
this.methods = []
}
async onActivation(): Promise<void> {
setTimeout(async () => {
const version = await this.call('isogit', 'version')
if(version){
//this.call('terminal', 'log', version)
}else{
//this.call('terminal', 'log', 'Git is not installed on the system. Using builtin git instead. Performance will be affected. It is better to install git on the system and configure the credentials to connect to GitHub etc.')
}
}, 5000)
}
}

@ -0,0 +1,15 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class electronTemplates extends ElectronPlugin {
constructor() {
super({
displayName: 'electronTemplates',
name: 'electronTemplates',
description: 'templates',
})
}
onActivation(): void {
}
}

@ -0,0 +1,11 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class xtermPlugin extends ElectronPlugin {
constructor(){
super({
displayName: 'xterm',
name: 'xterm',
description: 'xterm',
})
}
}

@ -4,6 +4,7 @@ import { AstNode } from "@remix-project/remix-solidity"
import { CodeParser } from "../code-parser" import { CodeParser } from "../code-parser"
import { antlr } from '../types' import { antlr } from '../types'
import { pathToFileURL } from 'url' import { pathToFileURL } from 'url'
import isElectron from 'is-electron'
const SolidityParser = (window as any).SolidityParser = (window as any).SolidityParser || [] const SolidityParser = (window as any).SolidityParser = (window as any).SolidityParser || []
@ -45,7 +46,7 @@ export default class CodeParserAntlrService {
this.worker = new Worker(new URL('./antlr-worker', import.meta.url)) this.worker = new Worker(new URL('./antlr-worker', import.meta.url))
this.worker.postMessage({ this.worker.postMessage({
cmd: 'load', cmd: 'load',
url: document.location.protocol + '//' + document.location.host + '/assets/js/parser/antlr.js', url: isElectron()? 'assets/js/parser/antlr.js': document.location.protocol + '//' + document.location.host + '/assets/js/parser/antlr.js',
}); });
const self = this const self = this

@ -1,7 +1,8 @@
'use strict' 'use strict'
import { CodeParser } from "../code-parser"; import { CodeParser } from "../code-parser";
import isElectron from 'is-electron'
export type CodeParserImportsData= { export type CodeParserImportsData = {
files?: string[], files?: string[],
modules?: string[], modules?: string[],
packages?: string[], packages?: string[],
@ -16,7 +17,7 @@ export default class CodeParserImports {
this.init() this.init()
} }
async getImports(){ async getImports() {
return this.data return this.data
} }
@ -27,30 +28,43 @@ export default class CodeParserImports {
.filter(x => x !== '') .filter(x => x !== '')
.map(x => x.replace('./node_modules/', '')) .map(x => x.replace('./node_modules/', ''))
.filter(x => { .filter(x => {
if(x.includes('@openzeppelin')) { if (x.includes('@openzeppelin')) {
return !x.includes('mock') return !x.includes('mock')
}else{ } else {
return true return true
} }
}) })
// get unique first words of the values in the array // get unique first words of the values in the array
this.data.packages = [...new Set(this.data.modules.map(x => x.split('/')[0]))] this.data.packages = [...new Set(this.data.modules.map(x => x.split('/')[0]))]
} }
setFileTree = async () => { setFileTree = async () => {
if (isElectron()) {
const files = await this.plugin.call('fs', 'glob', '/', '**/*.sol')
// only get path property of files
this.data.files = files.map(x => x.path)
} else {
this.data.files = await this.getDirectory('/') this.data.files = await this.getDirectory('/')
this.data.files = this.data.files.filter(x => x.endsWith('.sol') && !x.startsWith('.deps') && !x.startsWith('.git')) this.data.files = this.data.files.filter(x => x.endsWith('.sol') && !x.startsWith('.deps') && !x.startsWith('.git'))
}
} }
getDirectory = async (dir: string) => { getDirectory = async (dir: string) => {
console.log('getDirectorySEARCH', dir)
let result = [] let result = []
let files = {} let files = {}
try { try {
if (await this.plugin.call('fileManager', 'exists', dir)) { if (await this.plugin.call('fileManager', 'exists', dir)) {
files = await this.plugin.call('fileManager', 'readdir', dir) files = await this.plugin.call('fileManager', 'readdir', dir)
} }
} catch (e) {} } catch (e) { }
const fileArray = this.normalize(files) const fileArray = this.normalize(files)
for (const fi of fileArray) { for (const fi of fileArray) {
@ -63,10 +77,12 @@ export default class CodeParserImports {
} }
} }
} }
return result return result
} }
normalize = filesList => { normalize = filesList => {
console.log('normalize', filesList)
const folders = [] const folders = []
const files = [] const files = []
Object.keys(filesList || {}).forEach(key => { Object.keys(filesList || {}).forEach(key => {

@ -0,0 +1,30 @@
import { Plugin } from '@remixproject/engine'
import * as templateWithContent from '@remix-project/remix-ws-templates'
const profile = {
name: 'remix-templates',
displayName: 'remix-templates',
description: 'Remix Templates plugin',
methods: ['getTemplate', 'loadTemplateInNewWindow'],
}
export class TemplatesPlugin extends Plugin {
constructor() {
super(profile)
}
async getTemplate (template: string, opts?: any) {
const templateList = Object.keys(templateWithContent)
if (!templateList.includes(template)) return
// @ts-ignore
const files = await templateWithContent[template](opts)
return files
}
// electron only method
async loadTemplateInNewWindow (template: string, opts?: any) {
const files = await this.getTemplate(template, opts)
this.call('electronTemplates', 'loadTemplateInNewWindow', files)
}
}

@ -50,6 +50,7 @@ export class RemixdHandle extends WebsocketPlugin {
} }
async activate() { async activate() {
console.trace('activate remixd')
this.connectToLocalhost() this.connectToLocalhost()
return true return true
} }

@ -0,0 +1,4 @@
{
"electron.openFolder": "Open Folder",
"electron.recentFolders": "Recent Folders"
}

@ -10,6 +10,7 @@ import terminalJson from './terminal.json';
import udappJson from './udapp.json'; import udappJson from './udapp.json';
import solidityUnitTestingJson from './solidityUnitTesting.json'; import solidityUnitTestingJson from './solidityUnitTesting.json';
import permissionHandlerJson from './permissionHandler.json'; import permissionHandlerJson from './permissionHandler.json';
import electronJson from './electron.json';
export default { export default {
...debuggerJson, ...debuggerJson,
@ -24,4 +25,5 @@ export default {
...udappJson, ...udappJson,
...solidityUnitTestingJson, ...solidityUnitTestingJson,
...permissionHandlerJson, ...permissionHandlerJson,
...electronJson
} }

@ -3,6 +3,7 @@ import { EventEmitter } from 'events'
import { QueryParams } from '@remix-project/remix-lib' import { QueryParams } from '@remix-project/remix-lib'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import Registry from '../state/registry' import Registry from '../state/registry'
const isElectron = require('is-electron')
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const themes = [ const themes = [
@ -30,7 +31,7 @@ const profile = {
} }
export class ThemeModule extends Plugin { export class ThemeModule extends Plugin {
constructor () { constructor() {
super(profile) super(profile)
this.events = new EventEmitter() this.events = new EventEmitter()
this._deps = { this._deps = {
@ -40,7 +41,7 @@ export class ThemeModule extends Plugin {
themes.map((theme) => { themes.map((theme) => {
this.themes[theme.name.toLocaleLowerCase()] = { this.themes[theme.name.toLocaleLowerCase()] = {
...theme, ...theme,
url: window.location.origin + ( window.location.pathname.startsWith('/address/') || window.location.pathname.endsWith('.sol') ? '/' : window.location.pathname ) + theme.url url: isElectron() ? theme.url : window.location.origin + (window.location.pathname.startsWith('/address/') || window.location.pathname.endsWith('.sol') ? '/' : window.location.pathname) + theme.url
} }
}) })
this._paq = _paq this._paq = _paq
@ -58,22 +59,26 @@ export class ThemeModule extends Plugin {
/** Return the active theme /** Return the active theme
* @return {{ name: string, quality: string, url: string }} - The active theme * @return {{ name: string, quality: string, url: string }} - The active theme
*/ */
currentTheme () { currentTheme() {
if (isElectron()) {
const theme = 'https://remix.ethereum.org/' + this.themes[this.active].url.replace(/\\/g, '/').replace(/\/\//g, '/').replace(/\/$/g, '')
return { ...this.themes[this.active], url: theme }
}
return this.themes[this.active] return this.themes[this.active]
} }
/** Returns all themes as an array */ /** Returns all themes as an array */
getThemes () { getThemes() {
return Object.keys(this.themes).map(key => this.themes[key]) return Object.keys(this.themes).map(key => this.themes[key])
} }
/** /**
* Init the theme * Init the theme
*/ */
initTheme (callback) { // callback is setTimeOut in app.js which is always passed initTheme(callback) { // callback is setTimeOut in app.js which is always passed
if (callback) this.initCallback = callback if (callback) this.initCallback = callback
if (this.active) { if (this.active) {
document.getElementById('theme-link') ? document.getElementById('theme-link').remove():null document.getElementById('theme-link') ? document.getElementById('theme-link').remove() : null
const nextTheme = this.themes[this.active] // Theme const nextTheme = this.themes[this.active] // Theme
document.documentElement.style.setProperty('--theme', nextTheme.quality) document.documentElement.style.setProperty('--theme', nextTheme.quality)
@ -85,6 +90,7 @@ export class ThemeModule extends Plugin {
if (callback) callback() if (callback) callback()
}) })
document.head.insertBefore(theme, document.head.firstChild) document.head.insertBefore(theme, document.head.firstChild)
//if (callback) callback()
} }
} }
@ -92,7 +98,7 @@ export class ThemeModule extends Plugin {
* Change the current theme * Change the current theme
* @param {string} [themeName] - The name of the theme * @param {string} [themeName] - The name of the theme
*/ */
switchTheme (themeName) { switchTheme(themeName) {
themeName = themeName && themeName.toLocaleLowerCase() themeName = themeName && themeName.toLocaleLowerCase()
if (themeName && !Object.keys(this.themes).includes(themeName)) { if (themeName && !Object.keys(this.themes).includes(themeName)) {
throw new Error(`Theme ${themeName} doesn't exist`) throw new Error(`Theme ${themeName} doesn't exist`)
@ -102,7 +108,7 @@ export class ThemeModule extends Plugin {
_paq.push(['trackEvent', 'themeModule', 'switchTo', next]) _paq.push(['trackEvent', 'themeModule', 'switchTo', next])
const nextTheme = this.themes[next] // Theme const nextTheme = this.themes[next] // Theme
if (!this.forced) this._deps.config.set('settings/theme', next) if (!this.forced) this._deps.config.set('settings/theme', next)
document.getElementById('theme-link') ? document.getElementById('theme-link').remove():null document.getElementById('theme-link') ? document.getElementById('theme-link').remove() : null
const theme = document.createElement('link') const theme = document.createElement('link')
theme.setAttribute('rel', 'stylesheet') theme.setAttribute('rel', 'stylesheet')
@ -116,15 +122,21 @@ export class ThemeModule extends Plugin {
document.documentElement.style.setProperty('--theme', nextTheme.quality) document.documentElement.style.setProperty('--theme', nextTheme.quality)
if (themeName) this.active = themeName if (themeName) this.active = themeName
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
if (isElectron()) {
const theme = 'https://remix.ethereum.org/' + nextTheme.url.replace(/\\/g, '/').replace(/\/\//g, '/').replace(/\/$/g, '')
this.emit('themeChanged', { ...nextTheme, url: theme })
this.events.emit('themeChanged', { ...nextTheme, url: theme })
} else {
this.emit('themeChanged', nextTheme) this.emit('themeChanged', nextTheme)
this.events.emit('themeChanged', nextTheme) this.events.emit('themeChanged', nextTheme)
} }
}
/** /**
* fixes the invertion for images since this should be adjusted when we switch between dark/light qualified themes * fixes the invertion for images since this should be adjusted when we switch between dark/light qualified themes
* @param {element} [image] - the dom element which invert should be fixed to increase visibility * @param {element} [image] - the dom element which invert should be fixed to increase visibility
*/ */
fixInvert (image) { fixInvert(image) {
const invert = this.currentTheme().quality === 'dark' ? 1 : 0 const invert = this.currentTheme().quality === 'dark' ? 1 : 0
if (image) { if (image) {
image.style.filter = `invert(${invert})` image.style.filter = `invert(${invert})`

@ -23,6 +23,7 @@ import { Storage } from '@remix-project/remix-lib'
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
) )
})() })()

@ -2,10 +2,11 @@ import { PluginManager } from '@remixproject/engine'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { QueryParams } from '@remix-project/remix-lib' import { QueryParams } from '@remix-project/remix-lib'
import { IframePlugin } from '@remixproject/engine-web' import { IframePlugin } from '@remixproject/engine-web'
const isElectron = require('is-electron')
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
// requiredModule removes the plugin from the plugin manager list on UI // requiredModule removes the plugin from the plugin manager list on UI
const requiredModules = [ // services + layout views + system views let requiredModules = [ // services + layout views + system views
'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'locale', 'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'locale',
'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', 'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons',
'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity', 'solidity-logic', 'gistHandler', 'layout', 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity', 'solidity-logic', 'gistHandler', 'layout',
@ -14,6 +15,11 @@ const requiredModules = [ // services + layout views + system views
'vm-shanghai', 'vm-shanghai',
'compileAndRun', 'search', 'recorder', 'fileDecorator', 'codeParser', 'codeFormatter', 'solidityumlgen', 'contractflattener', 'solidity-script'] 'compileAndRun', 'search', 'recorder', 'fileDecorator', 'codeParser', 'codeFormatter', 'solidityumlgen', 'contractflattener', 'solidity-script']
if (isElectron()) {
requiredModules = [...requiredModules, 'fs', 'electronTemplates', 'isogit', 'remix-templates', 'electronconfig']
}
// dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd) // dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd)
const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither'] const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither']
@ -178,6 +184,7 @@ export class RemixAppManager extends PluginManager {
} }
return plugins.map(plugin => { return plugins.map(plugin => {
if (plugin.name === 'dgit') { plugin.url = 'https://dgit4-76cc9.web.app/' }
if (plugin.name === testPluginName) plugin.url = testPluginUrl if (plugin.name === testPluginName) plugin.url = testPluginUrl
return new IframePlugin(plugin) return new IframePlugin(plugin)
}) })

@ -20,6 +20,8 @@ export class RemixEngine extends Engine {
if (name === 'fetchAndCompile') return { queueTimeout: 60000 * 4 } if (name === 'fetchAndCompile') return { queueTimeout: 60000 * 4 }
if (name === 'walletconnect') return { queueTimeout: 60000 * 4 } if (name === 'walletconnect') return { queueTimeout: 60000 * 4 }
if (name === 'udapp') return { queueTimeout: 60000 * 4 } if (name === 'udapp') return { queueTimeout: 60000 * 4 }
if (name === 'fs') return { queueTimeout: 60000 * 4 }
if (name === 'isogit') return { queueTimeout: 60000 * 4 }
return { queueTimeout: 10000 } return { queueTimeout: 10000 }
} }

@ -15,24 +15,24 @@ const versionData = {
} }
const loadLocalSolJson = async () => { const loadLocalSolJson = async () => {
// execute apps/remix-ide/ci/downloadsoljson.sh //execute apps/remix-ide/ci/downloadsoljson.sh
const child = require('child_process').execSync('bash ./apps/remix-ide/ci/downloadsoljson.sh', { encoding: 'utf8', cwd: process.cwd(), shell: true }) const child = require('child_process').execSync('bash ' + __dirname + '/ci/downloadsoljson.sh', { encoding: 'utf8', cwd: process.cwd(), shell: true })
// show output // show output
console.log(child) console.log(child)
} }
fs.writeFileSync('./apps/remix-ide/src/assets/version.json', JSON.stringify(versionData)) fs.writeFileSync(__dirname + '/src/assets/version.json', JSON.stringify(versionData))
loadLocalSolJson() loadLocalSolJson()
const project = fs.readFileSync('./apps/remix-ide/project.json', 'utf8') const project = fs.readFileSync(__dirname + '/project.json', 'utf8')
const implicitDependencies = JSON.parse(project).implicitDependencies const implicitDependencies = JSON.parse(project).implicitDependencies
const copyPatterns = implicitDependencies.map((dep) => { const copyPatterns = implicitDependencies.map((dep) => {
try { try {
fs.statSync(__dirname + `/../../dist/apps/${dep}`).isDirectory() fs.statSync(__dirname + `/../../dist/apps/${dep}`).isDirectory()
return { from: `../../dist/apps/${dep}`, to: `plugins/${dep}` } return { from: __dirname + `/../../dist/apps/${dep}`, to: `plugins/${dep}` }
} }
catch (e) { catch (e) {
console.log('error', e) console.log('error', e)
@ -77,7 +77,11 @@ module.exports = composePlugins(withNx(), withReact(), (config) => {
} }
// add public path // add public path
if(process.env.NX_DESKTOP_FROM_DIST){
config.output.publicPath = './'
}else{
config.output.publicPath = '/' config.output.publicPath = '/'
}
// set filename // set filename
config.output.filename = `[name].${versionData.version}.${versionData.timestamp}.js` config.output.filename = `[name].${versionData.version}.${versionData.timestamp}.js`
@ -130,6 +134,7 @@ module.exports = composePlugins(withNx(), withReact(), (config) => {
ignored: /node_modules/ ignored: /node_modules/
} }
console.log('config', process.env.NX_DESKTOP_FROM_DIST)
return config; return config;
}); });

@ -0,0 +1,22 @@
# REMIX DESKTOP
## Development
In the main repo yarn, then run yarn serve
In this directory apps/remixdesktop, yarn, then run: yarn start:dev to boot the electron app
In chrome chrome://inspect/#devices you can add localhost:5858 to the network targets and then you will see an inspect button electron/js2c/browser_init
file:///
You can use that to inspect the output of the electron app
If you run into issues with yarn when native node modules are being rebuilt you need
- Windows: install Visual Studio Tools with Desktop Development C++ enabled in the Workloads
- MacOS: install Xcode or Xcode Command Line Tools
- Linux: unknown, probably a C++ compiler
## Builds
Builds can be found in the artefacts of CI.
## CI
CI will only run the builds is the branch is master or contains the word: desktop

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -0,0 +1,105 @@
{
"name": "remixdesktop",
"version": "1.0.0",
"main": "build/main.js",
"license": "MIT",
"type": "commonjs",
"description": "Remix IDE Desktop",
"repository": {
"type": "git",
"url": "git+https://github.com/ethereum/remix-project.git"
},
"author": {
"name": "Remix Team",
"email": "remix@ethereum.org"
},
"bugs": {
"url": "https://github.com/ethereum/remix-project/issues"
},
"homepage": "https://github.com/ethereum/remix-project#readme",
"appId": "org.ethereum.remixdesktop",
"mac": {
"category": "public.app-category.productivity"
},
"scripts": {
"start:dev": "tsc && cross-env NODE_ENV=development electron --inspect=5858 .",
"start:production": "tsc && cross-env NODE_ENV=production electron .",
"dist": "tsc && electron-builder",
"postinstall": "electron-builder install-app-deps"
},
"devDependencies": {
"@electron/rebuild": "^3.2.13",
"cross-env": "^7.0.3",
"electron": "^25.0.1",
"electron-builder": "^23.6.0",
"typescript": "^5.1.3"
},
"dependencies": {
"@remix-project/remix-ws-templates": "^1.0.19",
"@remixproject/engine": "0.3.37",
"@remixproject/engine-electron": "0.3.37",
"@remixproject/plugin": "0.3.37",
"@remixproject/plugin-electron": "0.3.37",
"@vscode/ripgrep": "^1.15.4",
"chokidar": "^3.5.3",
"glob": "9.3.5",
"isomorphic-git": "^1.24.2",
"node-pty": "^0.10.1"
},
"build": {
"productName": "Remix IDE",
"appId": "org.ethereum.remix-ide",
"asar": true,
"icon": "assets/icon.png",
"files": [
"build/**/*"
],
"mac": {
"category": "public.app-category.productivity",
"target": [
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
},
{
"target": "dmg",
"arch": [
"x64",
"arm64",
"universal"
]
}
],
"darkModeSupport": true
},
"dmg": {
"writeUpdateInfo": false
},
"nsis": {
"createDesktopShortcut": "always",
"allowToChangeInstallationDirectory": true,
"oneClick": false,
"shortcutName": "Remix IDE",
"differentialPackage": false
},
"win": {
"target": [
"nsis"
]
},
"linux": {
"target": [
"deb",
"snap",
"AppImage"
],
"category": "WebBrowser"
},
"directories": {
"output": "release"
}
}
}

@ -0,0 +1,61 @@
import { Engine, PluginManager } from '@remixproject/engine';
import { ipcMain } from 'electron';
import { FSPlugin } from './plugins/fsPlugin';
import { app } from 'electron';
import { XtermPlugin } from './plugins/xtermPlugin';
import git from 'isomorphic-git'
import { IsoGitPlugin } from './plugins/isoGitPlugin';
import { ConfigPlugin } from './plugins/configPlugin';
import { TemplatesPlugin } from './plugins/templates';
const engine = new Engine()
const appManager = new PluginManager()
const fsPlugin = new FSPlugin()
const xtermPlugin = new XtermPlugin()
const isoGitPlugin = new IsoGitPlugin()
const configPlugin = new ConfigPlugin()
const templatesPlugin = new TemplatesPlugin()
engine.register(appManager)
engine.register(fsPlugin)
engine.register(xtermPlugin)
engine.register(isoGitPlugin)
engine.register(configPlugin)
engine.register(templatesPlugin)
appManager.activatePlugin('electronconfig')
appManager.activatePlugin('fs')
ipcMain.handle('manager:activatePlugin', async (event, plugin) => {
return await appManager.call(plugin, 'createClient', event.sender.id)
})
ipcMain.on('fs:openFolder', async (event) => {
fsPlugin.openFolder(event)
})
ipcMain.on('terminal:new', async (event) => {
xtermPlugin.new(event)
})
ipcMain.on('template:open', async (event) => {
templatesPlugin.openTemplate(event)
})
ipcMain.on('git:startclone', async (event) => {
isoGitPlugin.startClone(event)
})
ipcMain.handle('getWebContentsID', (event, message) => {
return event.sender.id
})
app.on('before-quit', async (event) => {
await appManager.call('fs', 'removeCloseListener')
await appManager.call('fs', 'closeWatch')
await appManager.call('xterm', 'closeTerminals')
})

@ -0,0 +1,114 @@
import { app, BrowserWindow, dialog, Menu, MenuItem } from 'electron';
import path from 'path';
export let isPackaged = false;
if (
process.mainModule &&
process.mainModule.filename.indexOf('app.asar') !== -1
) {
isPackaged = true;
} else if (process.argv.filter(a => a.indexOf('app.asar') !== -1).length > 0) {
isPackaged = true;
}
// get system home dir
const homeDir = app.getPath('userData')
const windowSet = new Set<BrowserWindow>([]);
export const createWindow = async (dir?: string): Promise<void> => {
// Create the browser window.
const mainWindow = new BrowserWindow({
height: 800,
width: 1024,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
},
});
if (dir && dir.endsWith('/')) dir = dir.slice(0, -1)
let params = dir ? `?opendir=${encodeURIComponent(dir)}` : '';
// and load the index.html of the app.
mainWindow.loadURL(
process.env.NODE_ENV === 'production' || isPackaged ? `file://${__dirname}/remix-ide/index.html` + params :
'http://localhost:8080' + params)
mainWindow.maximize();
if (dir) {
mainWindow.setTitle(dir)
}
// on close
mainWindow.on('close', (event) => {
windowSet.delete(mainWindow)
})
windowSet.add(mainWindow)
//mainWindow.webContents.openDevTools();
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
require('./engine')
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
const showAbout = () => {
void dialog.showMessageBox({
title: `About Remix`,
message: `Remix`,
detail: `Remix`,
buttons: [],
});
};
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
const isMac = process.platform === 'darwin'
import FileMenu from './menus/file';
import darwinMenu from './menus/darwin';
import WindowMenu from './menus/window';
import EditMenu from './menus/edit';
import GitMenu from './menus/git';
import ViewMenu from './menus/view';
import { execCommand } from './menus/commands';
const commandKeys: Record<string, string> = {
'window:new': 'CmdOrCtrl+N',
'folder:open': 'CmdOrCtrl+O',
};
const menu = [...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []),
FileMenu(commandKeys, execCommand),
GitMenu(commandKeys, execCommand),
EditMenu(commandKeys, execCommand),
ViewMenu(commandKeys, execCommand),
WindowMenu(commandKeys, execCommand, []),
]
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))

@ -0,0 +1,39 @@
import {app, Menu, BrowserWindow, ipcMain} from 'electron';
import { createWindow } from '../main';
const commands: Record<string, (focusedWindow?: BrowserWindow) => void> = {
'window:new': () => {
// If window is created on the same tick, it will consume event too
setTimeout(createWindow, 0);
},
'folder:open': (focusedWindow) => {
if (focusedWindow) {
ipcMain.emit('fs:openFolder', focusedWindow.webContents.id);
}
},
'terminal:new': (focusedWindow) => {
if (focusedWindow) {
ipcMain.emit('terminal:new', focusedWindow.webContents.id);
}
},
'template:open': (focusedWindow) => {
if (focusedWindow) {
ipcMain.emit('template:open', focusedWindow.webContents.id);
}
},
'git:startclone': (focusedWindow) => {
if (focusedWindow) {
ipcMain.emit('git:startclone', focusedWindow.webContents.id);
}
}
};
export const execCommand = (command: string, focusedWindow?: BrowserWindow) => {
const fn = commands[command];
if (fn) {
fn(focusedWindow);
}
};

@ -0,0 +1,28 @@
// This menu label is overrided by OSX to be the appName
// The label is set to appName here so it matches actual behavior
import {app, BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
showAbout: () => void
): MenuItemConstructorOptions => {
return {
label: `${app.name}`,
submenu: [
{
label: 'About Remix',
click() {
showAbout();
}
},
{
type: 'separator'
},
{
role: 'quit',
label: 'Quit Remix'
}
]
};
};

@ -0,0 +1,53 @@
import { BrowserWindow, MenuItemConstructorOptions } from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
) => {
const submenu: MenuItemConstructorOptions[] = [
{
role: 'copy',
command: 'editor:copy',
accelerator: commandKeys['editor:copy'],
registerAccelerator: true
} as any,
{
role: 'paste',
accelerator: commandKeys['editor:paste'],
registerAccelerator: true
},
{
role: 'cut',
accelerator: commandKeys['editor:cut'],
registerAccelerator: true
},
{
role: 'selectAll',
accelerator: commandKeys['editor:selectall'],
registerAccelerator: true
},
{
role: 'undo',
accelerator: commandKeys['editor:undo'],
registerAccelerator: true
},
];
if (process.platform !== 'darwin') {
submenu.push(
{ type: 'separator' },
{
label: 'Preferences...',
accelerator: commandKeys['window:preferences'],
click() {
execCommand('window:preferences');
}
}
);
}
return {
label: 'Edit',
submenu
};
};

@ -0,0 +1,49 @@
import { BrowserWindow, MenuItemConstructorOptions } from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin';
return {
label: 'File',
submenu: [
{
label: 'New Window',
accelerator: commandKeys['window:new'],
click(item, focusedWindow) {
execCommand('window:new', focusedWindow);
}
},
{
label: 'Open Folder',
accelerator: commandKeys['folder:open'],
click(item, focusedWindow) {
execCommand('folder:open', focusedWindow);
}
},
{
label: 'Load Template in New Window',
click(item, focusedWindow) {
execCommand('template:open', focusedWindow);
}
},
{
role: 'recentDocuments',
submenu: [
{
role: 'clearRecentDocuments'
}
]
},
{
role: 'close',
accelerator: commandKeys['window:close']
},
{
role: 'quit',
}
]
};
};

@ -0,0 +1,20 @@
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin';
return {
label: 'Git',
submenu: [
{
label: 'Clone Repository in New Window',
click(item, focusedWindow) {
execCommand('git:startclone', focusedWindow);
}
}
]
};
};

@ -0,0 +1,26 @@
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin';
return {
label: 'REMIX',
submenu: [
{
label: 'Close',
accelerator: commandKeys['pane:close'],
click(item, focusedWindow) {
execCommand('pane:close', focusedWindow);
}
},
{
label: isMac ? 'Close Window' : 'Quit',
role: 'close',
accelerator: commandKeys['window:close']
}
]
};
};

@ -0,0 +1,20 @@
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin';
return {
label: 'Terminal',
submenu: [
{
label: 'New Terminal',
click(item, focusedWindow) {
execCommand('terminal:new', focusedWindow);
}
}
]
};
};

@ -0,0 +1,87 @@
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin';
return {
label: 'View',
submenu: [
{
label: 'Toggle Developer Tools',
accelerator: (function() {
if (process.platform === 'darwin')
return 'Alt+Command+I';
else
return 'Ctrl+Shift+I';
})(),
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.webContents.toggleDevTools();
}
},
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.reload();
}
},
{
label: 'Toggle Full Screen',
accelerator: (function() {
if (process.platform === 'darwin')
return 'Ctrl+Command+F';
else
return 'F11';
})(),
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
},
{
label: 'Zoom In',
accelerator: 'CmdOrCtrl+=',
click: function(item, focusedWindow) {
if (focusedWindow){
let factor = focusedWindow.webContents.getZoomFactor()
if (factor < 4) {
factor = factor + 0.25
focusedWindow.webContents.setZoomFactor(factor)
}
}
}
},
{
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click: function(item, focusedWindow) {
if (focusedWindow){
let factor = focusedWindow.webContents.getZoomFactor()
if (factor > 1.25) {
factor = factor - 1.25
focusedWindow.webContents.setZoomFactor(factor)
}
}
}
},
{
label: 'Reset Zoom',
accelerator: 'CmdOrCtrl+0',
click: function(item, focusedWindow) {
if (focusedWindow)
{
focusedWindow.webContents.setZoomFactor(1)
}
}
},
]
};
};

@ -0,0 +1,63 @@
import { BrowserWindow, MenuItemConstructorOptions } from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
openedWindows: BrowserWindow[]
): MenuItemConstructorOptions => {
const submenu: MenuItemConstructorOptions[] = [
{
role: 'minimize',
accelerator: commandKeys['window:minimize']
},
{
type: 'separator'
},
{
// It's the same thing as clicking the green traffc-light on macOS
role: 'zoom',
accelerator: commandKeys['window:zoom']
},
{
type: 'separator'
},
{
type: 'separator'
},
{
role: 'front'
},
{
label: 'Toggle Always on Top',
click: (item, focusedWindow) => {
execCommand('window:toggleKeepOnTop', focusedWindow);
}
},
{
role: 'togglefullscreen',
accelerator: commandKeys['window:toggleFullScreen']
},
{
type: 'separator'
},
]
if(openedWindows.length > 1) {
submenu.push({
label: 'Close',
accelerator: commandKeys['pane:close'],
click(item, focusedWindow) {
execCommand('pane:close', focusedWindow);
}
})
}
return {
role: 'window',
id: 'window',
submenu
}
};

@ -0,0 +1,50 @@
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import { Profile } from "@remixproject/plugin-utils";
import { readConfig, writeConfig } from "../utils/config";
const profile: Profile = {
displayName: 'electronconfig',
name: 'electronconfig',
description: 'Electron Config'
}
export class ConfigPlugin extends ElectronBasePlugin {
clients: ConfigPluginClient[] = []
constructor() {
super(profile, clientProfile, ConfigPluginClient)
this.methods = [...super.methods, 'writeConfig', 'readConfig']
}
async writeConfig(data: any): Promise<void> {
writeConfig(data)
}
async readConfig(webContentsId: any): Promise<any> {
return readConfig()
}
}
const clientProfile: Profile = {
name: 'electronconfig',
displayName: 'electronconfig',
description: 'Electron Config',
methods: ['writeConfig', 'readConfig']
}
class ConfigPluginClient extends ElectronBasePluginClient {
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
}
async writeConfig(data: any): Promise<void> {
writeConfig(data)
}
async readConfig(): Promise<any> {
return readConfig()
}
}

@ -0,0 +1,377 @@
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import fs from 'fs/promises'
import { Profile } from "@remixproject/plugin-utils";
import chokidar from 'chokidar'
import { dialog } from "electron";
import { createWindow, isPackaged } from "../main";
import { writeConfig } from "../utils/config";
import { glob, GlobOptions } from 'glob'
import { Path } from 'path-scurry'
import path from "path";
const profile: Profile = {
displayName: 'fs',
name: 'fs',
description: 'fs'
}
const convertPathToPosix = (pathName: string): string => {
return pathName.split(path.sep).join(path.posix.sep)
}
const getBaseName = (pathName: string): string => {
return path.basename(pathName)
}
export class FSPlugin extends ElectronBasePlugin {
clients: FSPluginClient[] = []
constructor() {
super(profile, clientProfile, FSPluginClient)
this.methods = [...super.methods, 'closeWatch', 'removeCloseListener']
}
async onActivation(): Promise<void> {
const config = await this.call('electronconfig' as any, 'readConfig')
const openedFolders = config && config.openedFolders || []
this.call('electronconfig', 'writeConfig', { 'openedFolders': openedFolders })
const foldersToDelete: string[] = []
if (openedFolders && openedFolders.length) {
for (const folder of openedFolders) {
try {
const stat = await fs.stat(folder)
if (stat.isDirectory()) {
createWindow(folder)
}
} catch (e) {
console.log('error opening folder', folder, e)
foldersToDelete.push(folder)
}
}
if (foldersToDelete.length) {
const newFolders = openedFolders.filter((f: string) => !foldersToDelete.includes(f))
this.call('electronconfig', 'writeConfig', { 'recentFolders': newFolders })
}
}else{
createWindow()
}
}
async removeCloseListener(): Promise<void> {
for (const client of this.clients) {
client.window.removeAllListeners()
}
}
async closeWatch(): Promise<void> {
for (const client of this.clients) {
await client.closeWatch()
}
}
openFolder(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId)
if (client) {
client.openFolder()
}
}
}
const clientProfile: Profile = {
name: 'fs',
displayName: 'fs',
description: 'fs',
methods: ['readdir', 'readFile', 'writeFile', 'mkdir', 'rmdir', 'unlink', 'rename', 'stat', 'lstat', 'exists', 'currentPath', 'watch', 'closeWatch', 'setWorkingDir', 'openFolder', 'openFolderInSameWindow', 'getRecentFolders', 'removeRecentFolder', 'glob', 'openWindow', 'selectFolder']
}
class FSPluginClient extends ElectronBasePluginClient {
watcher: chokidar.FSWatcher
workingDir: string = ''
trackDownStreamUpdate: Record<string, string> = {}
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
this.onload(() => {
if(!isPackaged) {
this.window.webContents.openDevTools()
}
this.window.on('close', async () => {
await this.removeFromOpenedFolders(this.workingDir)
await this.closeWatch()
})
})
}
// best for non recursive
async readdir(path: string): Promise<string[]> {
// call node fs.readdir
if (!path) return []
const startTime = Date.now()
const files = await fs.readdir(this.fixPath(path), {
withFileTypes: true
})
const result: any[] = []
for (const file of files) {
const isDirectory = file.isDirectory()
result.push({
file: file.name,
isDirectory
})
}
return result
}
async glob(path: string, pattern: string, options?: GlobOptions): Promise<string[] | Path[]> {
path = convertPathToPosix(this.fixPath(path))
const files = await glob(path + pattern, {
withFileTypes: true,
...options
})
const result: any[] = []
for (const file of files) {
let pathWithoutWorkingDir = (file as Path).path.replace(this.workingDir, '')
if (!pathWithoutWorkingDir.endsWith('/')) {
pathWithoutWorkingDir = pathWithoutWorkingDir + '/'
}
if (pathWithoutWorkingDir.startsWith('/')) {
pathWithoutWorkingDir = pathWithoutWorkingDir.slice(1)
}
if(pathWithoutWorkingDir.startsWith('\\')) {
pathWithoutWorkingDir = pathWithoutWorkingDir.slice(1)
}
result.push({
path: pathWithoutWorkingDir + (file as Path).name,
isDirectory: (file as Path).isDirectory(),
})
}
return result
}
async readFile(path: string, options: any): Promise<string | undefined> {
// hacky fix for TS error
if (!path) return undefined
try {
return (fs as any).readFile(this.fixPath(path), options)
} catch (e) {
return undefined
}
}
async writeFile(path: string, content: string, options: any): Promise<void> {
this.trackDownStreamUpdate[path] = content
return (fs as any).writeFile(this.fixPath(path), content, options)
}
async mkdir(path: string): Promise<void> {
return fs.mkdir(this.fixPath(path))
}
async rmdir(path: string): Promise<void> {
return fs.rm(this.fixPath(path), {
recursive: true
})
}
async unlink(path: string): Promise<void> {
return fs.unlink(this.fixPath(path))
}
async rename(oldPath: string, newPath: string): Promise<void> {
return fs.rename(this.fixPath(oldPath), this.fixPath(newPath))
}
async stat(path: string): Promise<any> {
try {
const stat = await fs.stat(this.fixPath(path))
const isDirectory = stat.isDirectory()
return {
...stat,
isDirectoryValue: isDirectory
}
} catch (e) {
return undefined
}
}
async lstat(path: string): Promise<any> {
try {
const stat = await fs.lstat(this.fixPath(path))
const isDirectory = stat.isDirectory()
return {
...stat,
isDirectoryValue: isDirectory
}
} catch (e) {
return undefined
}
}
async exists(path: string): Promise<boolean> {
return fs.access(this.fixPath(path)).then(() => true).catch(() => false)
}
async currentPath(): Promise<string> {
return process.cwd()
}
async watch(): Promise<void> {
if (this.watcher) this.watcher.close()
this.watcher =
chokidar.watch(this.workingDir, {
ignorePermissionErrors: true, ignoreInitial: true,
ignored: [
'**/node_modules/**',
'**/.git/index.lock', // this file is created and unlinked all the time when git is running on Windows
]
}).on('all', async (eventName, path, stats) => {
let pathWithoutPrefix = path.replace(this.workingDir, '')
pathWithoutPrefix = convertPathToPosix(pathWithoutPrefix)
if (pathWithoutPrefix.startsWith('/')) pathWithoutPrefix = pathWithoutPrefix.slice(1)
if (eventName === 'change') {
// remove workingDir from path
const newContent = await fs.readFile(path, 'utf-8')
const currentContent = this.trackDownStreamUpdate[pathWithoutPrefix]
if (currentContent !== newContent) {
try {
this.emit('change', eventName, pathWithoutPrefix)
} catch (e) {
console.log('error emitting change', e)
}
}
} else {
try {
this.emit('change', eventName, pathWithoutPrefix)
} catch (e) {
console.log('error emitting change', e)
}
}
})
}
async closeWatch(): Promise<void> {
if (this.watcher) this.watcher.close()
}
async updateRecentFolders(path: string): Promise<void> {
const config = await this.call('electronconfig' as any, 'readConfig')
config.recentFolders = config.recentFolders || []
config.recentFolders = config.recentFolders.filter((p: string) => p !== path)
config.recentFolders.push(path)
writeConfig(config)
}
async updateOpenedFolders(path: string): Promise<void> {
const config = await this.call('electronconfig' as any, 'readConfig')
config.openedFolders = config.openedFolders || []
config.openedFolders = config.openedFolders.filter((p: string) => p !== path)
config.openedFolders.push(path)
writeConfig(config)
}
async removeFromOpenedFolders(path: string): Promise<void> {
const config = await this.call('electronconfig' as any, 'readConfig')
config.openedFolders = config.openedFolders || []
config.openedFolders = config.openedFolders.filter((p: string) => p !== path)
writeConfig(config)
}
async getRecentFolders(): Promise<string[]> {
const config = await this.call('electronconfig' as any, 'readConfig')
return config.recentFolders || []
}
async removeRecentFolder(path: string): Promise<void> {
const config = await this.call('electronconfig' as any, 'readConfig')
config.recentFolders = config.recentFolders || []
config.recentFolders = config.recentFolders.filter((p: string) => p !== path)
writeConfig(config)
}
async selectFolder(path?: string): Promise<string> {
let dirs: string[] | undefined
if (!path) {
dirs = dialog.showOpenDialogSync(this.window, {
properties: ['openDirectory', 'createDirectory', "showHiddenFiles"]
})
}
path = dirs && dirs.length && dirs[0] ? dirs[0] : path
if (!path) return ''
return path
}
async openFolder(path?: string): Promise<void> {
let dirs: string[] | undefined
if (!path) {
dirs = dialog.showOpenDialogSync(this.window, {
properties: ['openDirectory', 'createDirectory', "showHiddenFiles"]
})
}
path = dirs && dirs.length && dirs[0] ? dirs[0] : path
if (!path) return
await this.updateRecentFolders(path)
await this.updateOpenedFolders(path)
this.openWindow(path)
}
async openFolderInSameWindow(path?: string): Promise<void> {
let dirs: string[] | undefined
if (!path) {
dirs = dialog.showOpenDialogSync(this.window, {
properties: ['openDirectory', 'createDirectory', "showHiddenFiles"]
})
}
path = dirs && dirs.length && dirs[0] ? dirs[0] : path
if (!path) return
this.workingDir = path
await this.updateRecentFolders(path)
await this.updateOpenedFolders(path)
this.window.setTitle(this.workingDir)
this.watch()
this.emit('workingDirChanged', path)
}
async setWorkingDir(path: string): Promise<void> {
this.workingDir = path
await this.updateRecentFolders(path)
await this.updateOpenedFolders(path)
this.window.setTitle(getBaseName(this.workingDir))
this.watch()
this.emit('workingDirChanged', path)
await this.call('fileManager', 'closeAllFiles')
}
fixPath(path: string): string {
if (this.workingDir === '') throw new Error('workingDir is not set')
if (path) {
if (path.startsWith('/')) {
path = path.slice(1)
}
}
path = this.workingDir + (!this.workingDir.endsWith('/') ? '/' : '') + path
return path
}
openWindow(path: string): void {
createWindow(path)
}
}

@ -0,0 +1,368 @@
import { PluginClient } from "@remixproject/plugin";
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import fs from 'fs/promises'
import git from 'isomorphic-git'
import { dialog } from "electron";
import http from 'isomorphic-git/http/web'
import { gitProxy } from "../tools/git";
const profile: Profile = {
name: 'isogit',
displayName: 'isogit',
description: 'isogit plugin',
}
export class IsoGitPlugin extends ElectronBasePlugin {
clients: IsoGitPluginClient[] = []
constructor() {
super(profile, clientProfile, IsoGitPluginClient)
}
startClone(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId)
if (client) {
client.startClone()
}
}
}
const parseInput = (input: any) => {
return {
corsProxy: 'https://corsproxy.remixproject.org/',
http,
onAuth: (url: any) => {
url
const auth = {
username: input.token,
password: ''
}
return auth
}
}
}
const clientProfile: Profile = {
name: 'isogit',
displayName: 'isogit',
description: 'isogit plugin',
methods: ['init', 'localStorageUsed', 'version', 'addremote', 'delremote', 'remotes', 'fetch', 'clone', 'export', 'import', 'status', 'log', 'commit', 'add', 'remove', 'reset', 'rm', 'lsfiles', 'readblob', 'resolveref', 'branches', 'branch', 'checkout', 'currentbranch', 'push', 'pin', 'pull', 'pinList', 'unPin', 'setIpfsConfig', 'zip', 'setItem', 'getItem', 'openFolder']
}
class IsoGitPluginClient extends ElectronBasePluginClient {
workingDir: string = ''
gitIsInstalled: boolean = false
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
this.onload(() => {
this.on('fs' as any, 'workingDirChanged', async (path: string) => {
this.workingDir = path
this.gitIsInstalled = await gitProxy.version() ? true : false
})
})
}
async version() {
return gitProxy.version()
}
async getGitConfig() {
return {
fs,
dir: this.workingDir,
}
}
async status(cmd: any) {
if (this.workingDir === '') {
return []
}
if (this.gitIsInstalled) {
const status = await gitProxy.status(this.workingDir)
return status
}
const status = await git.statusMatrix({
...await this.getGitConfig(),
...cmd
})
//console.log('STATUS', status, await this.getGitConfig())
return status
}
async log(cmd: any) {
/* we will use isomorphic git for now
if(this.gitIsInstalled){
const log = await gitProxy.log(this.workingDir, cmd.ref)
console.log('LOG', log)
return log
}
*/
if (this.workingDir === '') {
return []
}
const log = await git.log({
...await this.getGitConfig(),
...cmd
})
return log
}
async add(cmd: any) {
const add = await git.add({
...await this.getGitConfig(),
...cmd
})
return add
}
async rm(cmd: any) {
const rm = await git.remove({
...await this.getGitConfig(),
...cmd
})
return rm
}
async reset(cmd: any) {
const reset = await git.resetIndex({
...await this.getGitConfig(),
...cmd
})
return reset
}
async commit(cmd: any) {
if (this.gitIsInstalled) {
const status = await gitProxy.commit(this.workingDir, cmd.message)
return status
}
const commit = await git.commit({
...await this.getGitConfig(),
...cmd
})
return commit
}
async init(input: any) {
await git.init({
...await this.getGitConfig(),
defaultBranch: (input && input.branch) || 'main'
})
}
async branch(cmd: any) {
const branch = await git.branch({
...await this.getGitConfig(),
...cmd
})
return branch
}
async lsfiles(cmd: any) {
const lsfiles = await git.listFiles({
...await this.getGitConfig(),
...cmd
})
return lsfiles
}
async resolveref(cmd: any) {
const resolveref = await git.resolveRef({
...await this.getGitConfig(),
...cmd
})
return resolveref
}
async readblob(cmd: any) {
const readblob = await git.readBlob({
...await this.getGitConfig(),
...cmd
})
return readblob
}
async checkout(cmd: any) {
const checkout = await git.checkout({
...await this.getGitConfig(),
...cmd
})
return checkout
}
async push(cmd: any) {
if (this.gitIsInstalled) {
await gitProxy.push(this.workingDir, cmd.remote, cmd.ref, cmd.remoteRef, cmd.force)
} else {
const push = await git.push({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input)
})
return push
}
}
async pull(cmd: any) {
if (this.gitIsInstalled) {
await gitProxy.pull(this.workingDir, cmd.remote, cmd.ref, cmd.remoteRef)
} else {
const pull = await git.pull({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input)
})
return pull
}
}
async fetch(cmd: any) {
if (this.gitIsInstalled) {
await gitProxy.fetch(this.workingDir, cmd.remote, cmd.remoteRef)
} else {
const fetch = await git.fetch({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input)
})
return fetch
}
}
async clone(cmd: any) {
if (this.gitIsInstalled) {
await gitProxy.clone(cmd.url, cmd.dir)
} else {
try {
const clone = await git.clone({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input),
dir: cmd.dir || this.workingDir
})
return clone
} catch (e) {
console.log('CLONE ERROR', e)
throw e
}
}
}
async addremote(cmd: any) {
const addremote = await git.addRemote({
...await this.getGitConfig(),
...cmd
})
return addremote
}
async delremote(cmd: any) {
const delremote = await git.deleteRemote({
...await this.getGitConfig(),
...cmd
})
return delremote
}
remotes = async () => {
let remotes = []
remotes = await git.listRemotes({ ...await this.getGitConfig() })
return remotes
}
async currentbranch() {
try {
const defaultConfig = await this.getGitConfig()
const name = await git.currentBranch(defaultConfig)
return name
} catch (e) {
return ''
}
}
async branches() {
try {
let cmd: any = { ...await this.getGitConfig() }
const remotes = await this.remotes()
let branches = []
branches = (await git.listBranches(cmd)).map((branch) => { return { remote: undefined, name: branch } })
for (const remote of remotes) {
cmd = {
...cmd,
remote: remote.remote
}
const remotebranches = (await git.listBranches(cmd)).map((branch) => { return { remote: remote.remote, name: branch } })
branches = [...branches, ...remotebranches]
}
return branches
} catch (e) {
return []
}
}
async startClone() {
this.call('filePanel' as any, 'clone')
}
}

@ -0,0 +1,72 @@
import { PluginClient } from "@remixproject/plugin";
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import * as templateWithContent from '@remix-project/remix-ws-templates'
import fs from 'fs/promises'
import { createWindow } from "../main";
import path from 'path'
const profile: Profile = {
name: 'electronTemplates',
displayName: 'electronTemplates',
description: 'Templates plugin',
}
export class TemplatesPlugin extends ElectronBasePlugin {
clients: TemplatesPluginClient[] = []
constructor() {
super(profile, clientProfile, TemplatesPluginClient)
}
openTemplate(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId)
if (client) {
client.openTemplate()
}
}
}
const clientProfile: Profile = {
name: 'electronTemplates',
displayName: 'electronTemplates',
description: 'Templates plugin',
methods: ['loadTemplateInNewWindow', 'openTemplate'],
}
export type WorkspaceTemplate = 'gist-template' | 'code-template' | 'remixDefault' | 'blank' | 'ozerc20' | 'zeroxErc20' | 'ozerc721'
class TemplatesPluginClient extends ElectronBasePluginClient {
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
}
async loadTemplateInNewWindow (files: any) {
let folder = await this.call('fs' as any, 'selectFolder')
if (!folder || folder === '') return
// @ts-ignore
for (const file in files) {
try {
if(!folder.endsWith('/')) folder += '/'
await fs.mkdir(path.dirname(folder + file), { recursive: true})
await fs.writeFile(folder + file, files[file], {
encoding: 'utf8'
})
} catch (error) {
console.error(error)
}
}
createWindow(folder)
}
async openTemplate(){
this.call('filePanel' as any, 'loadTemplate')
}
}

@ -0,0 +1,153 @@
import { PluginClient } from "@remixproject/plugin";
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import os from 'os';
import * as pty from "node-pty"
import process from 'node:process';
import { userInfo } from 'node:os';
import { findExecutable } from "../utils/findExecutable";
export const detectDefaultShell = () => {
const { env } = process;
if (process.platform === 'win32') {
return env.SHELL || 'powershell.exe';
}
try {
const { shell } = userInfo();
if (shell) {
return shell;
}
} catch { }
if (process.platform === 'darwin') {
return env.SHELL || '/bin/zsh';
}
return env.SHELL || '/bin/sh';
};
// Stores default shell when imported.
const defaultShell = detectDefaultShell();
export default defaultShell;
const profile: Profile = {
name: 'xterm',
displayName: 'xterm',
description: 'xterm plugin',
}
export class XtermPlugin extends ElectronBasePlugin {
clients: XtermPluginClient[] = []
constructor() {
super(profile, clientProfile, XtermPluginClient)
this.methods = [...super.methods, 'closeTerminals']
}
new(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId)
if (client) {
client.new()
}
}
async closeTerminals(): Promise<void> {
for (const client of this.clients) {
await client.closeAll()
}
}
}
const clientProfile: Profile = {
name: 'xterm',
displayName: 'xterm',
description: 'xterm plugin',
methods: ['createTerminal', 'close', 'keystroke', 'getShells']
}
class XtermPluginClient extends ElectronBasePluginClient {
terminals: pty.IPty[] = []
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
this.onload(() => {
this.emit('loaded')
})
}
async keystroke(key: string, pid: number): Promise<void> {
this.terminals[pid].write(key)
}
async getShells(): Promise<string[]> {
if(os.platform() === 'win32') {
const bash = await findExecutable('bash.exe')
if(bash) {
const shells = ['powershell.exe', 'cmd.exe', ...bash]
// filter out duplicates
return shells.filter((v, i, a) => a.indexOf(v) === i)
}
return ['powershell.exe', 'cmd.exe']
}
return [defaultShell]
}
async createTerminal(path?: string, shell?: string): Promise<number> {
// filter undefined out of the env
const env = Object.keys(process.env)
.filter(key => process.env[key] !== undefined)
.reduce((env, key) => {
env[key] = process.env[key] || '';
return env;
}, {} as Record<string, string>);
const ptyProcess = pty.spawn(shell || defaultShell, [], {
name: 'xterm-color',
cols: 80,
rows: 20,
cwd: path || process.cwd(),
env: env
});
ptyProcess.onData((data: string) => {
this.sendData(data, ptyProcess.pid);
})
this.terminals[ptyProcess.pid] = ptyProcess
return ptyProcess.pid
}
async close(pid: number): Promise<void> {
this.terminals[pid].kill()
delete this.terminals[pid]
this.emit('close', pid)
}
async closeAll(): Promise<void> {
for (const pid in this.terminals) {
this.terminals[pid].kill()
delete this.terminals[pid]
this.emit('close', pid)
}
}
async sendData(data: string, pid: number) {
this.emit('data', data, pid)
}
async new(): Promise<void> {
}
}

@ -0,0 +1,33 @@
import { Message } from '@remixproject/plugin-utils'
import { contextBridge, ipcRenderer } from 'electron'
console.log('preload.ts')
/* preload script needs statically defined API for each plugin */
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates']
let webContentsId: number | undefined
ipcRenderer.invoke('getWebContentsID').then((id: number) => {
webContentsId = id
})
contextBridge.exposeInMainWorld('electronAPI', {
activatePlugin: (name: string) => {
return ipcRenderer.invoke('manager:activatePlugin', name)
},
getWindowId: () => ipcRenderer.invoke('getWindowID'),
plugins: exposedPLugins.map(name => {
return {
name,
on: (cb:any) => ipcRenderer.on(`${name}:send`, cb),
send: (message: Partial<Message>) => {
ipcRenderer.send(`${name}:on:${webContentsId}`, message)
}
}
})
})

@ -0,0 +1,151 @@
import { exec } from 'child_process';
import { CommitObject, ReadCommitResult } from 'isomorphic-git';
import { promisify } from 'util';
const execAsync = promisify(exec);
const statusTransFormMatrix = (status: string) => {
switch (status) {
case '??':
return [0, 2, 0]
case 'A ':
return [0, 2, 2]
case 'M ':
return [1, 2, 2]
case 'MM':
return [1, 2, 3]
case ' M':
return [1, 2, 0]
case ' D':
return [1, 0, 1]
case 'D ':
return [1, 0, 0]
case 'AM':
return [0, 2, 3]
default:
return [-1, -1, -1]
}
}
export const gitProxy = {
version: async () => {
try {
const result = await execAsync('git --version');
return result.stdout
} catch (error) {
return false;
}
},
clone: async (url: string, path: string) => {
const { stdout, stderr } = await execAsync(`git clone ${url} ${path}`);
},
async push(path: string, remote: string, src: string, branch: string, force: boolean = false) {
const { stdout, stderr } = await execAsync(`git push ${force ? ' -f' : ''} ${remote} ${src}:${branch}`, { cwd: path });
},
async pull(path: string, remote: string, src: string, branch: string) {
const { stdout, stderr } = await execAsync(`git pull ${remote} ${src}:${branch}`, { cwd: path });
},
async fetch(path: string, remote: string, branch: string) {
const { stdout, stderr } = await execAsync(`git fetch ${remote} ${branch}`, { cwd: path });
},
async commit(path: string, message: string) {
await execAsync(`git commit -m ${message}`, { cwd: path });
const { stdout, stderr } = await execAsync(`git rev-parse HEAD`, { cwd: path });
return stdout;
},
status: async (path: string) => {
const result = await execAsync('git status --porcelain -uall', { cwd: path })
//console.log('git status --porcelain -uall', result.stdout)
// parse the result.stdout
const lines = result.stdout.split('\n')
const files: any = []
const fileNames: any = []
//console.log('lines', lines)
lines.forEach((line: string) => {
// get the first two characters of the line
const status = line.slice(0, 2)
const file = line.split(' ').pop()
//console.log('line', line)
if (status && file) {
fileNames.push(file)
files.push([
file,
...statusTransFormMatrix(status)
])
}
}
)
// sort files by first column
files.sort((a: any, b: any) => {
if (a[0] < b[0]) {
return -1
}
if (a[0] > b[0]) {
return 1
}
return 0
})
return files
},
// buggy, doesn't work properly yet on windows
log: async (path: string, ref: string) => {
const result = await execAsync('git log ' + ref + ' --pretty=format:"{ oid:%H, message:"%s", author:"%an", email: "%ae", timestamp:"%at", tree: "%T", committer: "%cn", committer-email: "%ce", committer-timestamp: "%ct", parent: "%P" }" -n 20', { cwd: path })
console.log('git log', result.stdout)
const lines = result.stdout.split('\n')
const commits: ReadCommitResult[] = []
console.log('lines', lines)
lines.forEach((line: string) => {
console.log('line', normalizeJson(line))
line = normalizeJson(line)
const data = JSON.parse(line)
let commit: ReadCommitResult = {} as ReadCommitResult
commit.oid = data.oid
commit.commit = {} as CommitObject
commit.commit.message = data.message
commit.commit.tree = data.tree
commit.commit.committer = {} as any
commit.commit.committer.name = data.committer
commit.commit.committer.email = data['committer-email']
commit.commit.committer.timestamp = data['committer-timestamp']
commit.commit.author = {} as any
commit.commit.author.name = data.author
commit.commit.author.email = data.email
commit.commit.author.timestamp = data.timestamp
commit.commit.parent = [data.parent]
console.log('commit', commit)
commits.push(commit)
})
return commits
}
}
function normalizeJson(str: string) {
return str.replace(/[\s\n\r\t]/gs, '').replace(/,([}\]])/gs, '$1')
.replace(/([,{\[]|)(?:("|'|)([\w_\- ]+)\2:|)("|'|)(.*?)\4([,}\]])/gs, (str, start, q1, index, q2, item, end) => {
item = item.replace(/"/gsi, '').trim();
if (index) { index = '"' + index.replace(/"/gsi, '').trim() + '"'; }
if (!item.match(/^[0-9]+(\.[0-9]+|)$/) && !['true', 'false'].includes(item)) { item = '"' + item + '"'; }
if (index) { return start + index + ':' + item + end; }
return start + item + end;
});
}

@ -0,0 +1,39 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
const cacheDir = path.join(os.homedir(), '.cache_remix_ide')
console.log('cacheDir', cacheDir)
try {
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir)
}
if(!fs.existsSync(cacheDir + '/remixdesktop.json')) {
fs.writeFileSync(cacheDir + '/remixdesktop.json', JSON.stringify({}))
}
} catch (e) {
}
export const writeConfig = (data: any) => {
const cache = readConfig()
try {
fs.writeFileSync(cacheDir + '/remixdesktop.json', JSON.stringify({ ...cache, ...data }))
} catch (e) {
console.error('Can\'t write config file', e)
}
}
export const readConfig = () => {
if (fs.existsSync(cacheDir + '/remixdesktop.json')) {
try {
// read the cache file
const cache = fs.readFileSync(cacheDir + '/remixdesktop.json')
const data = JSON.parse(cache.toString())
return data
} catch (e) {
console.error('Can\'t read config file', e)
}
}
return undefined
}

@ -0,0 +1,86 @@
import path from "path";
import process from "process";
import { Stats } from "fs";
import fs from 'fs/promises'
export async function findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string[]> {
// If we have an absolute path then we take it.
if (path.isAbsolute(command)) {
return [command];
}
if (cwd === undefined) {
cwd = process.cwd();
}
const dir = path.dirname(command);
if (dir !== '.') {
// We have a directory and the directory is relative (see above). Make the path absolute
// to the current working directory.
return [path.join(cwd, command)];
}
if (paths === undefined && typeof process.env['PATH'] === 'string') {
paths = (process && process.env['PATH'] && process.env['PATH'].split(path.delimiter)) || [];
}
// No PATH environment. Make path absolute to the cwd.
if (paths === undefined || paths.length === 0) {
return [];
}
async function fileExists(path: string): Promise<boolean> {
try {
if (await fs.stat(path)) {
let statValue: Stats | undefined;
try {
statValue = await fs.stat(path);
} catch (e: any) {
if (e.message.startsWith('EACCES')) {
// it might be symlink
statValue = await fs.lstat(path);
}
}
return statValue ? !statValue.isDirectory() : false;
}
} catch (e) {
}
return false;
}
// We have a simple file name. We get the path variable from the env
// and try to find the executable on the path.
const results = [];
for (const pathEntry of paths) {
// The path entry is absolute.
let fullPath: string;
if (path.isAbsolute(pathEntry)) {
fullPath = path.join(pathEntry, command);
} else {
fullPath = path.join(cwd, pathEntry, command);
}
if (await fileExists(fullPath)) {
results.push(fullPath);
}
let withExtension = fullPath + '.com';
if (await fileExists(withExtension)) {
results.push(withExtension);
}
withExtension = fullPath + '.exe';
if (await fileExists(withExtension)) {
results.push(withExtension);
}
}
if (results.length > 0) {
return results;
}
return [];
}

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"skipLibCheck": true,
"esModuleInterop": true,
"noImplicitAny": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"strictPropertyInitialization": false,
"strict": true,
"outDir": "build",
"rootDir": "./src/",
"noEmitOnError": true,
"typeRoots": ["node_modules/@types", "./types"]
}
}

File diff suppressed because it is too large Load Diff

@ -1,6 +1,7 @@
/* global fetch */ /* global fetch */
'use strict' 'use strict'
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import isElectron from 'is-electron'
interface StringByString { interface StringByString {
[key: string]: string; [key: string]: string;
@ -118,7 +119,7 @@ export class GistHandler extends Plugin {
const path = element.replace(/\.\.\./g, '/') const path = element.replace(/\.\.\./g, '/')
obj['/gist-' + gistId + '/' + path] = data.files[element] obj['/gist-' + gistId + '/' + path] = data.files[element]
}) })
this.call('fileManager', 'setBatchFiles', obj, 'workspace', true, async (errorSavingFiles: any) => { this.call('fileManager', 'setBatchFiles', obj, isElectron()? 'electron':'workspace', true, async (errorSavingFiles: any) => {
if (errorSavingFiles) { if (errorSavingFiles) {
const modalContent = { const modalContent = {
id: 'gisthandler', id: 'gisthandler',

@ -10,6 +10,7 @@ import DialogViewPlugin from './components/modals/dialogViewPlugin'
import { AppContext } from './context/context' import { AppContext } from './context/context'
import { IntlProvider } from 'react-intl' import { IntlProvider } from 'react-intl'
import { CustomTooltip } from '@remix-ui/helper'; import { CustomTooltip } from '@remix-ui/helper';
import { RemixUiXterminals } from '@remix-ui/xterm'
interface IRemixAppUi { interface IRemixAppUi {
app: any app: any

@ -55,7 +55,7 @@ function HomeTabFeatured() {
</div> </div>
<div className="mx-1 px-1 d-flex"> <div className="mx-1 px-1 d-flex">
<a href="https://www.youtube.com/@EthereumRemix/videos" target="__blank"> <a href="https://www.youtube.com/@EthereumRemix/videos" target="__blank">
<img src={"/assets/img/YouTubeLogo.webp"} style={{ flex: "1", height: "170px", maxWidth: "170px" }} alt="" ></img> <img src={"assets/img/YouTubeLogo.webp"} style={{ flex: "1", height: "170px", maxWidth: "170px" }} alt="" ></img>
</a> </a>
<div className="h6 w-50 p-2 pl-4 align-self-center" style={{ flex: "1" }}> <div className="h6 w-50 p-2 pl-4 align-self-center" style={{ flex: "1" }}>
<h5><FormattedMessage id='home.remixYouTube' /></h5> <h5><FormattedMessage id='home.remixYouTube' /></h5>
@ -73,7 +73,7 @@ function HomeTabFeatured() {
</div> </div>
<div className="mx-1 px-1 d-flex"> <div className="mx-1 px-1 d-flex">
<a href="https://docs.google.com/forms/d/e/1FAIpQLSd0WsJnKbeJo-BGrnf7WijxAdmE4PnC_Z4M0IApbBfHLHZdsQ/viewform" target="__blank"> <a href="https://docs.google.com/forms/d/e/1FAIpQLSd0WsJnKbeJo-BGrnf7WijxAdmE4PnC_Z4M0IApbBfHLHZdsQ/viewform" target="__blank">
<img src={"/assets/img/remixRewardBetaTester_small.webp"} style={{ flex: "1", height: "170px", maxWidth: "170px" }} alt="" ></img> <img src={"assets/img/remixRewardBetaTester_small.webp"} style={{ flex: "1", height: "170px", maxWidth: "170px" }} alt="" ></img>
</a> </a>
<div className="h6 w-50 p-2 pl-4 align-self-center" style={{ flex: "1" }}> <div className="h6 w-50 p-2 pl-4 align-self-center" style={{ flex: "1" }}>
<h5><FormattedMessage id='home.betaTesting' /></h5> <h5><FormattedMessage id='home.betaTesting' /></h5>

@ -59,7 +59,7 @@ function HomeTabFeaturedPlugins ({plugin}: HomeTabFeaturedPluginsProps) {
} }
const startSolidity = async () => { const startSolidity = async () => {
await plugin.appManager.activatePlugin(['solidity', 'udapp', 'solidityStaticAnalysis', 'solidityUnitTesting']) //await plugin.appManager.activatePlugin(['solidity', 'udapp', 'solidityStaticAnalysis', 'solidityUnitTesting'])
plugin.verticalIcons.select('solidity') plugin.verticalIcons.select('solidity')
_paq.push(['trackEvent', 'hometabActivate', 'userActivate', 'solidity']) _paq.push(['trackEvent', 'hometabActivate', 'userActivate', 'solidity'])
} }

@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useState, useRef, useReducer } from 'react'
import { FormattedMessage } from 'react-intl'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
const _paq = window._paq = window._paq || [] // eslint-disable-line
import { CustomTooltip } from '@remix-ui/helper';
interface HomeTabFileProps {
plugin: any
}
export const HomeTabFileElectron = ({ plugin }: HomeTabFileProps) => {
const loadTemplate = async () => {
plugin.call('filePanel', 'loadTemplate')
}
const clone = async () => {
plugin.call('filePanel', 'clone')
}
const importFromGist = () => {
_paq.push(['trackEvent', 'hometab', 'filesSection', 'importFromGist'])
plugin.call('gistHandler', 'load', '')
plugin.verticalIcons.select('filePanel')
}
return (
<div className="justify-content-start mt-1 p-2 d-flex flex-column" id="hTFileSection">
<label style={{ fontSize: "1.2rem" }}><FormattedMessage id='home.files' /></label>
<label style={{ fontSize: "0.8rem" }} className="pt-2"><FormattedMessage id='home.loadFrom' /></label>
<div className="d-flex">
<button className="btn p-2 border mr-2" data-id="landingPageImportFromGistButton" onClick={async () => await loadTemplate()}>Project Template</button>
<button className="btn p-2 border mr-2" data-id="landingPageImportFromGistButton" onClick={async () => await clone()}>Clone a Git Repository</button>
<button className="btn p-2 border mr-2" data-id="landingPageImportFromGistButton" onClick={() => importFromGist()}>Gist</button>
</div>
</div>
)
}

@ -7,6 +7,7 @@ import Carousel from 'react-multi-carousel'
import WorkspaceTemplate from './workspaceTemplate' import WorkspaceTemplate from './workspaceTemplate'
import 'react-multi-carousel/lib/styles.css' import 'react-multi-carousel/lib/styles.css'
import CustomNavButtons from './customNavButtons' import CustomNavButtons from './customNavButtons'
import isElectron from 'is-electron'
declare global { declare global {
interface Window { interface Window {
_paq: any _paq: any
@ -58,6 +59,12 @@ function HomeTabGetStarted ({plugin}: HomeTabGetStartedProps) {
} }
const createWorkspace = async (templateName) => { const createWorkspace = async (templateName) => {
if(isElectron()){
await plugin.call('remix-templates', 'loadTemplateInNewWindow', templateName)
return
}
await plugin.appManager.activatePlugin('filePanel') await plugin.appManager.activatePlugin('filePanel')
const timeStamp = Date.now() const timeStamp = Date.now()
let templateDisplayName = TEMPLATE_NAMES[templateName] let templateDisplayName = TEMPLATE_NAMES[templateName]

@ -9,6 +9,8 @@ import HomeTabScamAlert from './components/homeTabScamAlert'
import HomeTabGetStarted from './components/homeTabGetStarted' import HomeTabGetStarted from './components/homeTabGetStarted'
import HomeTabFeatured from './components/homeTabFeatured' import HomeTabFeatured from './components/homeTabFeatured'
import HomeTabFeaturedPlugins from './components/homeTabFeaturedPlugins' import HomeTabFeaturedPlugins from './components/homeTabFeaturedPlugins'
import isElectron from 'is-electron'
import { HomeTabFileElectron } from './components/homeTabFileElectron'
declare global { declare global {
interface Window { interface Window {
@ -50,7 +52,9 @@ export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {
<div className='d-flex flex-row w-100 custom_home_bg'> <div className='d-flex flex-row w-100 custom_home_bg'>
<div className="px-2 pl-3 justify-content-start d-flex border-right flex-column" id="remixUIHTLeft" style={{ width: 'inherit' }}> <div className="px-2 pl-3 justify-content-start d-flex border-right flex-column" id="remixUIHTLeft" style={{ width: 'inherit' }}>
<HomeTabTitle /> <HomeTabTitle />
<HomeTabFile plugin={plugin} /> {!isElectron()?
<HomeTabFile plugin={plugin} />:
<HomeTabFileElectron plugin={plugin}></HomeTabFileElectron>}
<HomeTabLearn plugin={plugin} /> <HomeTabLearn plugin={plugin} />
</div> </div>
<div className="pl-2 pr-3 justify-content-start d-flex flex-column" style={{width: "65%"}} id="remixUIHTRight"> <div className="pl-2 pr-3 justify-content-start d-flex flex-column" style={{width: "65%"}} id="remixUIHTRight">

@ -18,13 +18,13 @@ const DragBar = (props: IRemixDragBarUi) => {
function stopDrag (e: MouseEvent, data: any) { function stopDrag (e: MouseEvent, data: any) {
const h = window.innerHeight - data.y const h = window.innerHeight - data.y
props.refObject.current.setAttribute('style', `height: ${h}px;`) props.refObject.current.setAttribute('style', `height: ${h}px;`)
setDragBarPosY(window.innerHeight - props.refObject.current.offsetHeight) setDragBarPosY(props.refObject.current.offsetTop)
setDragState(false) setDragState(false)
props.setHideStatus(false) props.setHideStatus(false)
} }
const handleResize = () => { const handleResize = () => {
if (!props.refObject.current) return if (!props.refObject.current) return
setDragBarPosY(window.innerHeight - props.refObject.current.offsetHeight) setDragBarPosY(props.refObject.current.offsetTop)
} }
useEffect(() => { useEffect(() => {

@ -1,7 +1,7 @@
.mainview { .mainview {
display : flex; display : flex;
flex-direction : column; flex-direction : column;
height : 100%; height : 70%;
width : 100%; width : 100%;
position : relative; position : relative;
} }

@ -29,7 +29,6 @@ export const ResultItem = (props: ResultItemProps) => {
useEffect(() => { useEffect(() => {
if (props.file.forceReload) { if (props.file.forceReload) {
console.log('force reload')
clearTimeout(reloadTimeOut.current) clearTimeout(reloadTimeOut.current)
clearTimeout(loadTimeout.current) clearTimeout(loadTimeout.current)
subscribed.current = true subscribed.current = true

@ -1,9 +1,18 @@
import { EOL } from 'os' import { EOL } from 'os'
import { SearchResultLineLine } from '../../types' import { SearchResultLineLine } from '../../types'
import isElectron from 'is-electron'
export const getDirectory = async (dir: string, plugin: any) => { export const getDirectory = async (dir: string, plugin: any) => {
let result = [] let result = []
if (isElectron()) {
const files = await plugin.call('fs', 'glob', dir, '**/*')
// only get path property of files
result = files.map(x => x.path)
} else {
const files = await plugin.call('fileManager', 'readdir', dir) const files = await plugin.call('fileManager', 'readdir', dir)
const fileArray = normalize(files) const fileArray = normalize(files)
for (const fi of fileArray) { for (const fi of fileArray) {
@ -16,10 +25,11 @@ export const getDirectory = async (dir: string, plugin: any) => {
} }
} }
} }
return result
} }
return result
}
const normalize = filesList => { const normalize = filesList => {
const folders = [] const folders = []
const files = [] const files = []
Object.keys(filesList || {}).forEach(key => { Object.keys(filesList || {}).forEach(key => {
@ -36,7 +46,7 @@ export const getDirectory = async (dir: string, plugin: any) => {
} }
}) })
return [...folders, ...files] return [...folders, ...files]
} }
export const findLinesInStringWithMatch = (str: string, re: RegExp) => { export const findLinesInStringWithMatch = (str: string, re: RegExp) => {
return str return str
@ -54,7 +64,7 @@ export const findLinesInStringWithMatch = (str: string, re: RegExp) => {
const matchesInString = (str: string, re: RegExp) => { const matchesInString = (str: string, re: RegExp) => {
let a: RegExpExecArray let a: RegExpExecArray
const results:RegExpExecArray[] = []; const results: RegExpExecArray[] = [];
while ((a = re.exec(str || '')) !== null) { while ((a = re.exec(str || '')) !== null) {
results.push(a); results.push(a);
} }
@ -63,11 +73,11 @@ const matchesInString = (str: string, re: RegExp) => {
const splitLines = (matchResult: RegExpExecArray[], lineNumber: number) => { const splitLines = (matchResult: RegExpExecArray[], lineNumber: number) => {
return matchResult.map((matchResultPart, i) => { return matchResult.map((matchResultPart, i) => {
const result:SearchResultLineLine = { const result: SearchResultLineLine = {
left: matchResultPart.input.substring(0, matchResultPart.index), left: matchResultPart.input.substring(0, matchResultPart.index),
right: matchResultPart.input.substring(matchResultPart.index + matchResultPart[0].length), right: matchResultPart.input.substring(matchResultPart.index + matchResultPart[0].length),
center: matchResultPart[0], center: matchResultPart[0],
position : { position: {
start: { start: {
line: lineNumber, line: lineNumber,
column: matchResultPart.index, column: matchResultPart.index,
@ -92,7 +102,7 @@ function getEOL(text) {
return u > w ? '\n' : '\r\n'; return u > w ? '\n' : '\r\n';
} }
export const replaceAllInFile = (string: string, re:RegExp, newText: string) => { export const replaceAllInFile = (string: string, re: RegExp, newText: string) => {
return string.replace(re, newText) return string.replace(re, newText)
} }

@ -193,6 +193,7 @@ export const SearchProvider = ({
}, },
findText: async (path: string) => { findText: async (path: string) => {
if (!plugin) return if (!plugin) return
try { try {
if (state.find.length < 1) return if (state.find.length < 1) return
@ -327,6 +328,10 @@ export const SearchProvider = ({
setFiles(await getDirectory('/', plugin)) setFiles(await getDirectory('/', plugin))
}) })
plugin.on('fs', 'workingDirChanged', async () => {
setFiles(await getDirectory('/', plugin))
})
plugin.on('fileManager', 'fileAdded', async file => { plugin.on('fileManager', 'fileAdded', async file => {
setFiles(await getDirectory('/', plugin)) setFiles(await getDirectory('/', plugin))
await reloadStateForFile(file) await reloadStateForFile(file)

@ -13,6 +13,7 @@ import { configFileContent } from './compilerConfiguration'
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
import './css/style.css' import './css/style.css'
import isElectron from 'is-electron'
const defaultPath = "compiler_config.json" const defaultPath = "compiler_config.json"
declare global { declare global {
@ -560,7 +561,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
// "Uncaught RangeError: Maximum call stack size exceeded" error on Chromium, // "Uncaught RangeError: Maximum call stack size exceeded" error on Chromium,
// resort to non-worker version in that case. // resort to non-worker version in that case.
if (selectedVersion === 'builtin') selectedVersion = state.defaultVersion if (selectedVersion === 'builtin') selectedVersion = state.defaultVersion
if (selectedVersion !== 'builtin' && canUseWorker(selectedVersion)) { if (selectedVersion !== 'builtin' && (canUseWorker(selectedVersion) || isElectron())) {
compileTabLogic.compiler.loadVersion(true, url) compileTabLogic.compiler.loadVersion(true, url)
} else { } else {
compileTabLogic.compiler.loadVersion(false, url) compileTabLogic.compiler.loadVersion(false, url)

@ -9,6 +9,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import { format } from 'util' import { format } from 'util'
import './css/style.css' import './css/style.css'
import { CustomTooltip } from '@remix-ui/helper' import { CustomTooltip } from '@remix-ui/helper'
import isElectron from 'is-electron'
const _paq = (window as any)._paq = (window as any)._paq || [] // eslint-disable-line @typescript-eslint/no-explicit-any const _paq = (window as any)._paq = (window as any)._paq || [] // eslint-disable-line @typescript-eslint/no-explicit-any
@ -550,7 +551,7 @@ export const SolidityUnitTesting = (props: Record<string, any>) => { // eslint-d
currentCompilerUrl, currentCompilerUrl,
evmVersion, evmVersion,
optimize, optimize,
usingWorker: canUseWorker(currentVersion), usingWorker: canUseWorker(currentVersion) || isElectron(),
runs runs
} }
const deployCb = async (file: string, contractAddress: string) => { const deployCb = async (file: string, contractAddress: string) => {

@ -49,6 +49,7 @@ export const listenOnPluginEvents = (filePanelPlugin) => {
}) })
plugin.on('fileManager', 'rootFolderChanged', async (path: string) => { plugin.on('fileManager', 'rootFolderChanged', async (path: string) => {
console.log('rootFolderChanged', path)
rootFolderChanged(path) rootFolderChanged(path)
}) })
@ -96,6 +97,10 @@ export const listenOnProviderEvents = (provider) => (reducerDispatch: React.Disp
await switchToWorkspace(workspaceProvider.workspace) await switchToWorkspace(workspaceProvider.workspace)
}) })
provider.event.on('refresh', () => {
fetchWorkspaceDirectory('/')
})
provider.event.on('connected', () => { provider.event.on('connected', () => {
plugin.fileManager.setMode('localhost') plugin.fileManager.setMode('localhost')
dispatch(setMode('localhost')) dispatch(setMode('localhost'))
@ -108,7 +113,7 @@ export const listenOnProviderEvents = (provider) => (reducerDispatch: React.Disp
dispatch(loadLocalhostRequest()) dispatch(loadLocalhostRequest())
}) })
provider.event.on('fileExternallyChanged', (path: string, content: string) => { provider.event.on('fileExternallyChanged', (path: string, content: string, showAlert: boolean = true) => {
const config = plugin.registry.get('config').api const config = plugin.registry.get('config').api
const editor = plugin.registry.get('editor').api const editor = plugin.registry.get('editor').api
@ -117,6 +122,7 @@ export const listenOnProviderEvents = (provider) => (reducerDispatch: React.Disp
if (config.get('currentFile') === path) { if (config.get('currentFile') === path) {
// if it's the current file and the content is different: // if it's the current file and the content is different:
if(showAlert){
dispatch(displayNotification( dispatch(displayNotification(
path + ' changed', path + ' changed',
'This file has been changed outside of Remix IDE.', 'This file has been changed outside of Remix IDE.',
@ -124,7 +130,9 @@ export const listenOnProviderEvents = (provider) => (reducerDispatch: React.Disp
() => { () => {
editor.setText(path, content) editor.setText(path, content)
} }
)) ))}else{
editor.setText(path, content)
}
} else { } else {
// this isn't the current file, we can silently update the model // this isn't the current file, we can silently update the model
editor.setText(path, content) editor.setText(path, content)

@ -23,6 +23,7 @@ export type UrlParametersType = {
code: string, code: string,
url: string, url: string,
address: string address: string
opendir: string,
} }
const basicWorkspaceInit = async (workspaces: { name: string; isGitRepo: boolean; }[], workspaceProvider) => { const basicWorkspaceInit = async (workspaces: { name: string; isGitRepo: boolean; }[], workspaceProvider) => {
@ -50,9 +51,13 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
setPlugin(plugin, dispatch) setPlugin(plugin, dispatch)
const workspaceProvider = filePanelPlugin.fileProviders.workspace const workspaceProvider = filePanelPlugin.fileProviders.workspace
const localhostProvider = filePanelPlugin.fileProviders.localhost const localhostProvider = filePanelPlugin.fileProviders.localhost
const electrOnProvider = filePanelPlugin.fileProviders.electron
const params = queryParams.get() as UrlParametersType const params = queryParams.get() as UrlParametersType
const workspaces = await getWorkspaces() || [] let workspaces = []
if (!isElectron()) {
workspaces = await getWorkspaces() || []
dispatch(setWorkspaces(workspaces)) dispatch(setWorkspaces(workspaces))
}
if (params.gist) { if (params.gist) {
await createWorkspaceTemplate('gist-sample', 'gist-template') await createWorkspaceTemplate('gist-sample', 'gist-template')
plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false }) plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false })
@ -74,11 +79,11 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
let etherscanKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token') let etherscanKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token')
if (!etherscanKey) etherscanKey = '2HKUX5ZVASZIKWJM8MIQVCRUVZ6JAWT531' if (!etherscanKey) etherscanKey = '2HKUX5ZVASZIKWJM8MIQVCRUVZ6JAWT531'
const networks = [ const networks = [
{id: 1, name: 'mainnet'}, { id: 1, name: 'mainnet' },
{id: 3, name: 'ropsten'}, { id: 3, name: 'ropsten' },
{id: 4, name: 'rinkeby'}, { id: 4, name: 'rinkeby' },
{id: 42, name: 'kovan'}, { id: 42, name: 'kovan' },
{id: 5, name: 'goerli'} { id: 5, name: 'goerli' }
] ]
let found = false let found = false
const workspaceName = 'etherscan-code-sample' const workspaceName = 'etherscan-code-sample'
@ -107,15 +112,28 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
await workspaceProvider.set(filePath, data.compilationTargets[filePath]['content']) await workspaceProvider.set(filePath, data.compilationTargets[filePath]['content'])
} }
plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath)) plugin.on('editor', 'editorMounted', async () => await plugin.fileManager.openFile(filePath))
plugin.call('notification', 'toast', `Added ${count} verified contract${count === 1 ? '': 's'} from ${foundOnNetworks.join(',')} network${foundOnNetworks.length === 1 ? '': 's'} of Etherscan for contract address ${contractAddress} !!`) plugin.call('notification', 'toast', `Added ${count} verified contract${count === 1 ? '' : 's'} from ${foundOnNetworks.join(',')} network${foundOnNetworks.length === 1 ? '' : 's'} of Etherscan for contract address ${contractAddress} !!`)
} catch (error) { } catch (error) {
await basicWorkspaceInit(workspaces, workspaceProvider) await basicWorkspaceInit(workspaces, workspaceProvider)
} }
} else await basicWorkspaceInit(workspaces, workspaceProvider) } else await basicWorkspaceInit(workspaces, workspaceProvider)
} else if (isElectron()) { } else if (isElectron()) {
plugin.call('notification', 'toast', `connecting to localhost...`) if (params.opendir) {
await basicWorkspaceInit(workspaces, workspaceProvider) params.opendir = decodeURIComponent(params.opendir)
await plugin.call('manager', 'activatePlugin', 'remixd') plugin.call('notification', 'toast', `opening ${params.opendir}...`)
await plugin.call('fs', 'setWorkingDir', params.opendir)
}
plugin.setWorkspace({ name: 'electron', isLocalhost: false })
dispatch(setCurrentWorkspace({ name: 'electron', isGitRepo: false }))
electrOnProvider.init()
listenOnProviderEvents(electrOnProvider)(dispatch)
listenOnPluginEvents(plugin)
dispatch(setMode('browser'))
dispatch(fsInitializationCompleted())
plugin.emit('workspaceInitializationCompleted')
return
} else if (localStorage.getItem("currentWorkspace")) { } else if (localStorage.getItem("currentWorkspace")) {
const index = workspaces.findIndex(element => element.name == localStorage.getItem("currentWorkspace")) const index = workspaces.findIndex(element => element.name == localStorage.getItem("currentWorkspace"))
if (index !== -1) { if (index !== -1) {
@ -123,7 +141,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
workspaceProvider.setWorkspace(name) workspaceProvider.setWorkspace(name)
plugin.setWorkspace({ name: name, isLocalhost: false }) plugin.setWorkspace({ name: name, isLocalhost: false })
dispatch(setCurrentWorkspace({ name: name, isGitRepo: false })) dispatch(setCurrentWorkspace({ name: name, isGitRepo: false }))
}else{ } else {
_paq.push(['trackEvent', 'Storage', 'error', `Workspace in localstorage not found: ${localStorage.getItem("currentWorkspace")}`]) _paq.push(['trackEvent', 'Storage', 'error', `Workspace in localstorage not found: ${localStorage.getItem("currentWorkspace")}`])
await basicWorkspaceInit(workspaces, workspaceProvider) await basicWorkspaceInit(workspaces, workspaceProvider)
} }
@ -134,7 +152,13 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
listenOnPluginEvents(plugin) listenOnPluginEvents(plugin)
listenOnProviderEvents(workspaceProvider)(dispatch) listenOnProviderEvents(workspaceProvider)(dispatch)
listenOnProviderEvents(localhostProvider)(dispatch) listenOnProviderEvents(localhostProvider)(dispatch)
listenOnProviderEvents(electrOnProvider)(dispatch)
if (isElectron()) {
dispatch(setMode('browser')) dispatch(setMode('browser'))
} else {
dispatch(setMode('browser'))
}
plugin.setWorkspaces(await getWorkspaces()) plugin.setWorkspaces(await getWorkspaces())
dispatch(fsInitializationCompleted()) dispatch(fsInitializationCompleted())
plugin.emit('workspaceInitializationCompleted') plugin.emit('workspaceInitializationCompleted')
@ -181,7 +205,7 @@ export const publishToGist = async (path?: string, type?: string) => {
const accessToken = config.get('settings/gist-access-token') const accessToken = config.get('settings/gist-access-token')
if (!accessToken) { if (!accessToken) {
dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', null, () => {})) dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', null, () => { }))
} else { } else {
const params = queryParams.get() as SolidityConfiguration const params = queryParams.get() as SolidityConfiguration
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' + const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' +
@ -233,7 +257,7 @@ export const publishToGist = async (path?: string, type?: string) => {
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {})) dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => { }))
} }
} }
@ -266,7 +290,7 @@ export const createNewFolder = async (path: string, rootDir: string) => {
const exists = await fileManager.exists(dirName) const exists = await fileManager.exists(dirName)
if (exists) { if (exists) {
return dispatch(displayNotification('Failed to create folder', `A folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => {})) return dispatch(displayNotification('Failed to create folder', `A folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => { }))
} }
await fileManager.mkdir(dirName) await fileManager.mkdir(dirName)
path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path
@ -292,7 +316,7 @@ export const renamePath = async (oldPath: string, newPath: string) => {
const exists = await fileManager.exists(newPath) const exists = await fileManager.exists(newPath)
if (exists) { if (exists) {
dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => {})) dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => { }))
} else { } else {
await fileManager.rename(oldPath, newPath) await fileManager.rename(oldPath, newPath)
} }
@ -450,7 +474,7 @@ const handleGistResponse = (error, data) => {
if (data.html_url) { if (data.html_url) {
dispatch(displayNotification('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', 'Cancel', () => { dispatch(displayNotification('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', 'Cancel', () => {
window.open(data.html_url, '_blank') window.open(data.html_url, '_blank')
}, () => {})) }, () => { }))
} else { } else {
const error = JSON.stringify(data.errors, null, '\t') || '' const error = JSON.stringify(data.errors, null, '\t') || ''
const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message

@ -292,3 +292,10 @@ export const setGitConfig = (config: {username: string, token: string, email: st
payload: config payload: config
} }
} }
export const setElectronRecentFolders = (folders: string[]) => {
return {
type: 'SET_ELECTRON_RECENT_FOLDERS',
payload: folders
}
}

@ -2,7 +2,7 @@ import React from 'react'
import { bufferToHex } from '@ethereumjs/util' import { bufferToHex } from '@ethereumjs/util'
import { hash } from '@remix-project/remix-lib' import { hash } from '@remix-project/remix-lib'
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
import { addInputFieldSuccess, cloneRepositoryFailed, cloneRepositoryRequest, cloneRepositorySuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setCurrentWorkspaceBranches, setCurrentWorkspaceCurrentBranch, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace, setCurrentWorkspaceIsGitRepo, setGitConfig } from './payload' import { addInputFieldSuccess, cloneRepositoryFailed, cloneRepositoryRequest, cloneRepositorySuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setCurrentWorkspaceBranches, setCurrentWorkspaceCurrentBranch, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace, setCurrentWorkspaceIsGitRepo, setGitConfig, setElectronRecentFolders } from './payload'
import { addSlash, checkSlash, checkSpecialChars } from '@remix-ui/helper' import { addSlash, checkSlash, checkSpecialChars } from '@remix-ui/helper'
import { JSONStandardInput, WorkspaceTemplate } from '../types' import { JSONStandardInput, WorkspaceTemplate } from '../types'
@ -14,6 +14,7 @@ import { IndexedDBStorage } from '../../../../../../apps/remix-ide/src/app/files
import { getUncommittedFiles } from '../utils/gitStatusFilter' import { getUncommittedFiles } from '../utils/gitStatusFilter'
import { AppModal, ModalTypes } from '@remix-ui/app' import { AppModal, ModalTypes } from '@remix-ui/app'
import { contractDeployerScripts, etherscanScripts } from '@remix-project/remix-ws-templates' import { contractDeployerScripts, etherscanScripts } from '@remix-project/remix-ws-templates'
import isElectron from 'is-electron'
declare global { declare global {
interface Window { remixFileSystemCallback: IndexedDBStorage; } interface Window { remixFileSystemCallback: IndexedDBStorage; }
@ -22,6 +23,7 @@ declare global {
const LOCALHOST = ' - connect to localhost - ' const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - ' const NO_WORKSPACE = ' - none - '
const ELECTRON = 'electron'
const queryParams = new QueryParams() const queryParams = new QueryParams()
const _paq = window._paq = window._paq || [] //eslint-disable-line const _paq = window._paq = window._paq || [] //eslint-disable-line
let plugin, dispatch: React.Dispatch<any> let plugin, dispatch: React.Dispatch<any>
@ -83,6 +85,15 @@ const removeSlash = (s: string) => {
} }
export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, opts = null, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void, isGitRepo: boolean = false, createCommit: boolean = true) => { export const createWorkspace = async (workspaceName: string, workspaceTemplateName: WorkspaceTemplate, opts = null, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void, isGitRepo: boolean = false, createCommit: boolean = true) => {
if (isElectron()) {
if (workspaceTemplateName) {
await plugin.call('remix-templates', 'loadTemplateInNewWindow', workspaceTemplateName, opts)
}
return
}
await plugin.fileManager.closeAllFiles() await plugin.fileManager.closeAllFiles()
const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName) const promise = createWorkspaceTemplate(workspaceName, workspaceTemplateName)
dispatch(createWorkspaceRequest(promise)) dispatch(createWorkspaceRequest(promise))
@ -165,6 +176,7 @@ export type UrlParametersType = {
export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDefault', opts?) => { export const loadWorkspacePreset = async (template: WorkspaceTemplate = 'remixDefault', opts?) => {
const workspaceProvider = plugin.fileProviders.workspace const workspaceProvider = plugin.fileProviders.workspace
const electronProvider = plugin.fileProviders.electron
const params = queryParams.get() as UrlParametersType const params = queryParams.get() as UrlParametersType
switch (template) { switch (template) {
@ -267,12 +279,15 @@ export const workspaceExists = async (name: string) => {
} }
export const fetchWorkspaceDirectory = async (path: string) => { export const fetchWorkspaceDirectory = async (path: string) => {
if (!path) return if (!path) return
const provider = plugin.fileManager.currentFileProvider() const provider = plugin.fileManager.currentFileProvider()
const promise = new Promise((resolve) => { const promise = new Promise((resolve, reject) => {
provider.resolveDirectory(path, (error, fileTree) => { provider.resolveDirectory(path, (error, fileTree) => {
if (error) console.error(error) if (error) {
console.error(error)
return reject(error)
}
resolve(fileTree) resolve(fileTree)
}) })
}) })
@ -341,6 +356,12 @@ export const switchToWorkspace = async (name: string) => {
// if there is no other workspace, create remix default workspace // if there is no other workspace, create remix default workspace
plugin.call('notification', 'toast', `No workspace found! Creating default workspace ....`) plugin.call('notification', 'toast', `No workspace found! Creating default workspace ....`)
await createWorkspace('default_workspace', 'remixDefault') await createWorkspace('default_workspace', 'remixDefault')
} else if (name === ELECTRON) {
await plugin.fileProviders.workspace.setWorkspace(name)
await plugin.setWorkspace({ name, isLocalhost: false })
dispatch(setMode('browser'))
dispatch(setCurrentWorkspace({ name, isGitRepo: false }))
} else { } else {
const isActive = await plugin.call('manager', 'isActive', 'remixd') const isActive = await plugin.call('manager', 'isActive', 'remixd')
@ -406,8 +427,8 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
okFn: () => { okFn: () => {
loadFile(name, file, workspaceProvider, cb) loadFile(name, file, workspaceProvider, cb)
}, },
cancelFn: () => {}, cancelFn: () => { },
hideFn: () => {} hideFn: () => { }
} }
plugin.call('notification', 'modal', modalContent) plugin.call('notification', 'modal', modalContent)
} }
@ -415,7 +436,7 @@ export const uploadFile = async (target, targetFolder: string, cb?: (err: Error,
} }
export const uploadFolder = async (target, targetFolder: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { export const uploadFolder = async (target, targetFolder: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
for(const file of [...target.files]) { for (const file of [...target.files]) {
const workspaceProvider = plugin.fileProviders.workspace const workspaceProvider = plugin.fileProviders.workspace
const name = targetFolder === '/' ? file.webkitRelativePath : `${targetFolder}/${file.webkitRelativePath}` const name = targetFolder === '/' ? file.webkitRelativePath : `${targetFolder}/${file.webkitRelativePath}`
if (!await workspaceProvider.exists(name)) { if (!await workspaceProvider.exists(name)) {
@ -431,8 +452,8 @@ export const uploadFolder = async (target, targetFolder: string, cb?: (err: Erro
okFn: () => { okFn: () => {
loadFile(name, file, workspaceProvider, cb) loadFile(name, file, workspaceProvider, cb)
}, },
cancelFn: () => {}, cancelFn: () => { },
hideFn: () => {} hideFn: () => { }
} }
plugin.call('notification', 'modal', modalContent) plugin.call('notification', 'modal', modalContent)
} }
@ -484,6 +505,17 @@ export const cloneRepository = async (url: string) => {
const token = config.get('settings/gist-access-token') const token = config.get('settings/gist-access-token')
const repoConfig = { url, token } const repoConfig = { url, token }
if (isElectron()) {
try {
await plugin.call('dGitProvider', 'clone', repoConfig)
} catch (e) {
console.log(e)
plugin.call('notification', 'alert', {
id: 'cloneGitRepository',
message: e
})
}
} else {
try { try {
const repoName = await getRepositoryTitle(url) const repoName = await getRepositoryTitle(url)
@ -525,6 +557,7 @@ export const cloneRepository = async (url: string) => {
} catch (e) { } catch (e) {
dispatch(displayPopUp('An error occured: ' + e)) dispatch(displayPopUp('An error occured: ' + e))
} }
}
} }
export const checkGit = async () => { export const checkGit = async () => {
@ -574,7 +607,6 @@ export const getGitRepoCurrentBranch = async (workspaceName: string) => {
} }
export const showAllBranches = async () => { export const showAllBranches = async () => {
console.log('showAllBranches')
const isActive = await plugin.call('manager', 'isActive', 'dgit') const isActive = await plugin.call('manager', 'isActive', 'dgit')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit') if (!isActive) await plugin.call('manager', 'activatePlugin', 'dgit')
plugin.call('menuicons', 'select', 'dgit') plugin.call('menuicons', 'select', 'dgit')
@ -660,21 +692,21 @@ export const createNewBranch = async (branch: string) => {
export const createSolidityGithubAction = async () => { export const createSolidityGithubAction = async () => {
const path = '.github/workflows/run-solidity-unittesting.yml' const path = '.github/workflows/run-solidity-unittesting.yml'
await plugin.call('fileManager', 'writeFile', path , solTestYml) await plugin.call('fileManager', 'writeFile', path, solTestYml)
plugin.call('fileManager', 'open', path) plugin.call('fileManager', 'open', path)
} }
export const createTsSolGithubAction = async () => { export const createTsSolGithubAction = async () => {
const path = '.github/workflows/run-js-test.yml' const path = '.github/workflows/run-js-test.yml'
await plugin.call('fileManager', 'writeFile', path , tsSolTestYml) await plugin.call('fileManager', 'writeFile', path, tsSolTestYml)
plugin.call('fileManager', 'open', path) plugin.call('fileManager', 'open', path)
} }
export const createSlitherGithubAction = async () => { export const createSlitherGithubAction = async () => {
const path = '.github/workflows/run-slither-action.yml' const path = '.github/workflows/run-slither-action.yml'
await plugin.call('fileManager', 'writeFile', path , slitherYml) await plugin.call('fileManager', 'writeFile', path, slitherYml)
plugin.call('fileManager', 'open', path) plugin.call('fileManager', 'open', path)
} }
@ -738,6 +770,21 @@ export const checkoutRemoteBranch = async (branch: string, remote: string) => {
} }
} }
export const openElectronFolder = async (path: string) => {
await plugin.call('fs', 'openFolderInSameWindow', path)
}
export const getElectronRecentFolders = async () => {
const folders = await plugin.call('fs', 'getRecentFolders')
dispatch(setElectronRecentFolders(folders))
return folders
}
export const removeRecentElectronFolder = async (path: string) => {
await plugin.call('fs', 'removeRecentFolder', path)
await getElectronRecentFolders()
}
export const hasLocalChanges = async () => { export const hasLocalChanges = async () => {
const filesStatus = await plugin.call('dGitProvider', 'status') const filesStatus = await plugin.call('dGitProvider', 'status')
const uncommittedFiles = getUncommittedFiles(filesStatus) const uncommittedFiles = getUncommittedFiles(filesStatus)

@ -0,0 +1,65 @@
import React, { MouseEventHandler, useContext, useEffect, useState } from "react"
import { FileSystemContext } from "../contexts"
import isElectron from 'is-electron'
import { FormattedMessage } from "react-intl"
import '../css/electron-menu.css'
import { CustomTooltip } from '@remix-ui/helper'
export const ElectronMenu = () => {
const global = useContext(FileSystemContext)
useEffect(() => {
if (isElectron()) {
global.dispatchGetElectronRecentFolders()
}
}, [])
const openFolderElectron = async (path: string) => {
global.dispatchOpenElectronFolder(path)
}
const lastFolderName = (path: string) => {
const pathArray = path.split('/')
return pathArray[pathArray.length - 1]
}
return (
!isElectron() ? null :
(global.fs.browser.isSuccessfulWorkspace ? null :
<>
<div onClick={async () => { await openFolderElectron(null) }} className='btn btn-primary'><FormattedMessage id="electron.openFolder" /></div>
{global.fs.browser.recentFolders.length > 0 ?
<>
<label className="py-2 pt-3 align-self-center m-0">
<FormattedMessage id="electron.recentFolders" />
</label>
<ul>
{global.fs.browser.recentFolders.map((folder, index) => {
return <li key={index}>
<CustomTooltip
tooltipText={folder}
tooltipId={`electron-recent-folder-${index}`}
placement='bottom'
>
<div className="recentfolder pb-1">
<span onClick={async () => { await openFolderElectron(folder) }} className="pl-2 recentfolder_name pr-2">{lastFolderName(folder)}</span>
<span onClick={async () => { await openFolderElectron(folder) }} data-id={{ folder }} className="recentfolder_path pr-2">{folder}</span>
<i
onClick={() => {
global.dispatchRemoveRecentFolder(folder)
}}
className="fas fa-times recentfolder_delete pr-2"
>
</i>
</div>
</CustomTooltip>
</li>
})}
</ul>
</>
: null}
</>
)
)
}

@ -5,6 +5,7 @@ import { action, FileExplorerContextMenuProps } from '../types'
import '../css/file-explorer-context-menu.css' import '../css/file-explorer-context-menu.css'
import { customAction } from '@remixproject/plugin-api' import { customAction } from '@remixproject/plugin-api'
import UploadFile from './upload-file' import UploadFile from './upload-file'
import isElectron from 'is-electron'
declare global { declare global {
interface Window { interface Window {
@ -56,7 +57,8 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
} }
const itemMatchesCondition = (item: action, itemType: string, itemPath: string) => { const itemMatchesCondition = (item: action, itemType: string, itemPath: string) => {
if (item.type && Array.isArray(item.type) && (item.type.findIndex(name => name === itemType) !== -1)) return true if( isElectron() && item.platform && item.platform === 'browser') return false
else if (item.type && Array.isArray(item.type) && (item.type.findIndex(name => name === itemType) !== -1)) return true
else if (item.path && Array.isArray(item.path) && (item.path.findIndex(key => key === itemPath) !== -1)) return true else if (item.path && Array.isArray(item.path) && (item.path.findIndex(key => key === itemPath) !== -1)) return true
else if (item.extension && Array.isArray(item.extension) && (item.extension.findIndex(ext => itemPath.endsWith(ext)) !== -1)) return true else if (item.extension && Array.isArray(item.extension) && (item.extension.findIndex(ext => itemPath.endsWith(ext)) !== -1)) return true
else if (item.pattern && Array.isArray(item.pattern) && (item.pattern.filter(value => itemPath.match(new RegExp(value))).length > 0)) return true else if (item.pattern && Array.isArray(item.pattern) && (item.pattern.filter(value => itemPath.match(new RegExp(value))).length > 0)) return true

@ -46,6 +46,9 @@ export const FileSystemContext = createContext<{
dispatchCreateTsSolGithubAction: () => Promise<void>, dispatchCreateTsSolGithubAction: () => Promise<void>,
dispatchCreateSlitherGithubAction: () => Promise<void> dispatchCreateSlitherGithubAction: () => Promise<void>
dispatchCreateHelperScripts: (script: string) => Promise<void> dispatchCreateHelperScripts: (script: string) => Promise<void>
dispatchOpenElectronFolder: (path: string) => Promise<void>
dispatchGetElectronRecentFolders: () => Promise<void>
dispatchRemoveRecentFolder: (path: string) => Promise<void>
}>(null) }>(null)

@ -0,0 +1,27 @@
.recentfolder {
display: flex;
min-width: 0;
cursor: pointer;
}
.recentfolder_path {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.recentfolder_name {
flex-shrink: 0;
color: var(--text);
}
.recentfolder_name:hover {
color: var(--primary);
text-decoration: underline;
}
.recentfolder_delete {
flex-shrink: 0;
margin-left: auto;
color: var(--text);
}

@ -8,7 +8,7 @@ import { browserReducer, browserInitialState } from '../reducers/workspace'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, deleteAllWorkspaces, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, deleteAllWorkspaces, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder,
deletePath, renamePath, downloadPath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, deletePath, renamePath, downloadPath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace,
fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, uploadFolder, handleDownloadWorkspace, handleDownloadFiles, restoreBackupZip, cloneRepository, moveFile, moveFolder, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, uploadFolder, handleDownloadWorkspace, handleDownloadFiles, restoreBackupZip, cloneRepository, moveFile, moveFolder,
showAllBranches, switchBranch, createNewBranch, checkoutRemoteBranch, createSolidityGithubAction, createTsSolGithubAction, createSlitherGithubAction, createHelperScripts showAllBranches, switchBranch, createNewBranch, checkoutRemoteBranch, createSolidityGithubAction, createTsSolGithubAction, createSlitherGithubAction, createHelperScripts, openElectronFolder, getElectronRecentFolders, removeRecentElectronFolder
} from '../actions' } from '../actions'
import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types' import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -187,6 +187,19 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await createHelperScripts(script) await createHelperScripts(script)
} }
const dispatchOpenElectronFolder = async (path: string) => {
await openElectronFolder(path)
}
const dispatchGetElectronRecentFolders = async () => {
await getElectronRecentFolders()
}
const dispatchRemoveRecentFolder = async (path: string) => {
await removeRecentElectronFolder(path)
}
useEffect(() => { useEffect(() => {
dispatchInitWorkspace() dispatchInitWorkspace()
}, []) }, [])
@ -304,7 +317,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
dispatchCreateSolidityGithubAction, dispatchCreateSolidityGithubAction,
dispatchCreateTsSolGithubAction, dispatchCreateTsSolGithubAction,
dispatchCreateSlitherGithubAction, dispatchCreateSlitherGithubAction,
dispatchCreateHelperScripts dispatchCreateHelperScripts,
dispatchOpenElectronFolder,
dispatchGetElectronRecentFolders,
dispatchRemoveRecentFolder
} }
return ( return (
<FileSystemContext.Provider value={value}> <FileSystemContext.Provider value={value}>

@ -34,6 +34,7 @@ export interface BrowserState {
error: string error: string
}, },
fileState: fileDecoration[] fileState: fileDecoration[]
recentFolders: string[]
}, },
localhost: { localhost: {
sharedFolder: string, sharedFolder: string,
@ -86,7 +87,8 @@ export const browserInitialState: BrowserState = {
removedMenuItems: [], removedMenuItems: [],
error: null error: null
}, },
fileState: [] fileState: [],
recentFolders: []
}, },
localhost: { localhost: {
sharedFolder: '', sharedFolder: '',
@ -720,7 +722,7 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
} }
} }
case 'SET_GIT_CONFIG' : { case 'SET_GIT_CONFIG': {
const payload: { username: string, token: string, email: string } = action.payload const payload: { username: string, token: string, email: string } = action.payload
return { return {
...state, ...state,
@ -728,6 +730,17 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
} }
} }
case 'SET_ELECTRON_RECENT_FOLDERS': {
const payload: string[] = action.payload
return {
...state,
browser: {
...state.browser,
recentFolders: payload
}
}
}
default: default:
throw new Error() throw new Error()
@ -849,7 +862,6 @@ const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: s
const fetchWorkspaceDirectoryContent = (state: BrowserState, payload: { fileTree, path: string }): { [x: string]: Record<string, FileType> } => { const fetchWorkspaceDirectoryContent = (state: BrowserState, payload: { fileTree, path: string }): { [x: string]: Record<string, FileType> } => {
const files = normalize(payload.fileTree, ROOT_PATH) const files = normalize(payload.fileTree, ROOT_PATH)
return { [ROOT_PATH]: files } return { [ROOT_PATH]: files }
} }

@ -12,14 +12,17 @@ import { MenuItems, WorkSpaceState } from './types'
import { contextMenuActions } from './utils' import { contextMenuActions } from './utils'
import FileExplorerContextMenu from './components/file-explorer-context-menu' import FileExplorerContextMenu from './components/file-explorer-context-menu'
import { customAction } from '@remixproject/plugin-api' import { customAction } from '@remixproject/plugin-api'
import isElectron from 'is-electron'
import { ElectronMenu } from './components/electron-menu'
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const canUpload = window.File || window.FileReader || window.FileList || window.Blob const canUpload = window.File || window.FileReader || window.FileList || window.Blob
export function Workspace () { export function Workspace() {
const LOCALHOST = ' - connect to localhost - ' const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - ' const NO_WORKSPACE = ' - none - '
const ELECTRON = 'electron'
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE) const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
const [selectedWorkspace, setSelectedWorkspace] = useState<{ name: string, isGitRepo: boolean, branches?: { remote: any; name: string; }[], currentBranch?: string }>(null) const [selectedWorkspace, setSelectedWorkspace] = useState<{ name: string, isGitRepo: boolean, branches?: { remote: any; name: string; }[], currentBranch?: string }>(null)
const [showDropdown, setShowDropdown] = useState<boolean>(false) const [showDropdown, setShowDropdown] = useState<boolean>(false)
@ -101,6 +104,18 @@ export function Workspace () {
} }
setCurrentWorkspace(workspaceName) setCurrentWorkspace(workspaceName)
resetFocus() resetFocus()
// expose some UI to the plugin, perhaps not the best way to do it
if (global.plugin) {
global.plugin.loadTemplate = async () => {
await global.plugin.call('menuicons', 'select', 'filePanel')
createWorkspace()
}
global.plugin.clone = async () => {
await global.plugin.call('menuicons', 'select', 'filePanel')
cloneGitRepository()
}
}
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -109,8 +124,7 @@ export function Workspace () {
setCurrentWorkspace(global.fs.browser.currentWorkspace) setCurrentWorkspace(global.fs.browser.currentWorkspace)
global.dispatchFetchWorkspaceDirectory(ROOT_PATH) global.dispatchFetchWorkspaceDirectory(ROOT_PATH)
} }
else else {
{
setCurrentWorkspace(NO_WORKSPACE) setCurrentWorkspace(NO_WORKSPACE)
} }
@ -190,6 +204,7 @@ export function Workspace () {
const cloneGitRepository = () => { const cloneGitRepository = () => {
console.log('clone from workspace modal')
global.modal( global.modal(
intl.formatMessage({ id: 'filePanel.workspace.clone' }), intl.formatMessage({ id: 'filePanel.workspace.clone' }),
cloneModalMessage(), cloneModalMessage(),
@ -239,7 +254,7 @@ export function Workspace () {
try { try {
await global.dispatchRenameWorkspace(currentWorkspace, workspaceName) await global.dispatchRenameWorkspace(currentWorkspace, workspaceName)
} catch (e) { } catch (e) {
global.modal(intl.formatMessage({ id: 'filePanel.workspace.rename' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => {}, intl.formatMessage({ id: 'filePanel.cancel' })) global.modal(intl.formatMessage({ id: 'filePanel.workspace.rename' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => { }, intl.formatMessage({ id: 'filePanel.cancel' }))
console.error(e) console.error(e)
} }
} }
@ -248,7 +263,7 @@ export function Workspace () {
try { try {
await global.dispatchHandleDownloadWorkspace() await global.dispatchHandleDownloadWorkspace()
} catch (e) { } catch (e) {
global.modal(intl.formatMessage({ id: 'filePanel.workspace.download' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => {}, intl.formatMessage({ id: 'filePanel.cancel' })) global.modal(intl.formatMessage({ id: 'filePanel.workspace.download' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => { }, intl.formatMessage({ id: 'filePanel.cancel' }))
console.error(e) console.error(e)
} }
} }
@ -268,13 +283,13 @@ export function Workspace () {
// @ts-ignore: Object is possibly 'null'. // @ts-ignore: Object is possibly 'null'.
pausable: pausableCheckboxRef.current.checked, pausable: pausableCheckboxRef.current.checked,
// @ts-ignore: Object is possibly 'null'. // @ts-ignore: Object is possibly 'null'.
upgradeable: transparentRadioRef.current.checked ? transparentRadioRef.current.value : ( uupsRadioRef.current.checked ? uupsRadioRef.current.value : false ) upgradeable: transparentRadioRef.current.checked ? transparentRadioRef.current.value : (uupsRadioRef.current.checked ? uupsRadioRef.current.value : false)
} }
try { try {
await global.dispatchCreateWorkspace(workspaceName, workspaceTemplateName, opts, initGitRepo) await global.dispatchCreateWorkspace(workspaceName, workspaceTemplateName, opts, initGitRepo)
} catch (e) { } catch (e) {
global.modal(intl.formatMessage({ id: 'filePanel.workspace.create' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => {}, intl.formatMessage({ id: 'filePanel.cancel' })) global.modal(intl.formatMessage({ id: 'filePanel.workspace.create' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => { }, intl.formatMessage({ id: 'filePanel.cancel' }))
console.error(e) console.error(e)
} }
} }
@ -283,7 +298,7 @@ export function Workspace () {
try { try {
await global.dispatchDeleteWorkspace(global.fs.browser.currentWorkspace) await global.dispatchDeleteWorkspace(global.fs.browser.currentWorkspace)
} catch (e) { } catch (e) {
global.modal(intl.formatMessage({ id: 'filePanel.workspace.delete' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => {}, intl.formatMessage({ id: 'filePanel.cancel' })) global.modal(intl.formatMessage({ id: 'filePanel.workspace.delete' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => { }, intl.formatMessage({ id: 'filePanel.cancel' }))
console.error(e) console.error(e)
} }
} }
@ -292,7 +307,7 @@ export function Workspace () {
try { try {
await global.dispatchDeleteAllWorkspaces() await global.dispatchDeleteAllWorkspaces()
} catch (e) { } catch (e) {
global.modal(intl.formatMessage({ id: 'filePanel.workspace.deleteAll' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => {}, intl.formatMessage({ id: 'filePanel.cancel' })) global.modal(intl.formatMessage({ id: 'filePanel.workspace.deleteAll' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => { }, intl.formatMessage({ id: 'filePanel.cancel' }))
console.error(e) console.error(e)
} }
} }
@ -306,7 +321,7 @@ export function Workspace () {
await global.dispatchSwitchToWorkspace(name) await global.dispatchSwitchToWorkspace(name)
global.dispatchHandleExpandPath([]) global.dispatchHandleExpandPath([])
} catch (e) { } catch (e) {
global.modal(intl.formatMessage({ id: 'filePanel.workspace.switch' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => {}, intl.formatMessage({ id: 'filePanel.cancel' })) global.modal(intl.formatMessage({ id: 'filePanel.workspace.switch' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => { }, intl.formatMessage({ id: 'filePanel.cancel' }))
console.error(e) console.error(e)
} }
} }
@ -344,7 +359,7 @@ export function Workspace () {
intl.formatMessage({ id: 'filePanel.workspace.clone' }), intl.formatMessage({ id: 'filePanel.workspace.clone' }),
intl.formatMessage({ id: 'filePanel.workspace.cloneMessage' }), intl.formatMessage({ id: 'filePanel.workspace.cloneMessage' }),
intl.formatMessage({ id: 'filePanel.ok' }), intl.formatMessage({ id: 'filePanel.ok' }),
() => {}, () => { },
intl.formatMessage({ id: 'filePanel.cancel' }) intl.formatMessage({ id: 'filePanel.cancel' })
) )
} }
@ -370,7 +385,7 @@ export function Workspace () {
try { try {
global.dispatchDownloadPath(path) global.dispatchDownloadPath(path)
} catch (error) { } catch (error) {
global.modal('Download Failed', 'Unexpected error while downloading: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {}) global.modal('Download Failed', 'Unexpected error while downloading: ' + typeof error === 'string' ? error : error.message, 'Close', async () => { })
} }
} }
@ -378,7 +393,7 @@ export function Workspace () {
try { try {
global.dispatchCopyFile(src, dest) global.dispatchCopyFile(src, dest)
} catch (error) { } catch (error) {
global.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => {}) global.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => { })
} }
} }
@ -386,7 +401,7 @@ export function Workspace () {
try { try {
global.dispatchCopyFolder(src, dest) global.dispatchCopyFolder(src, dest)
} catch (error) { } catch (error) {
global.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => {}) global.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => { })
} }
} }
@ -458,15 +473,15 @@ export function Workspace () {
const pushChangesToGist = (path?: string, type?: string) => { const pushChangesToGist = (path?: string, type?: string) => {
global.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {}) global.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => { })
} }
const publishFolderToGist = (path?: string, type?: string) => { const publishFolderToGist = (path?: string, type?: string) => {
global.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) global.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => { })
} }
const publishFileToGist = (path?: string, type?: string) => { const publishFileToGist = (path?: string, type?: string) => {
global.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) global.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => { })
} }
const deleteMessage = (path: string[]) => { const deleteMessage = (path: string[]) => {
@ -484,14 +499,13 @@ export function Workspace () {
if (global.fs.readonly) return global.toast('cannot delete file. ' + name + ' is a read only explorer') if (global.fs.readonly) return global.toast('cannot delete file. ' + name + ' is a read only explorer')
if (!Array.isArray(path)) path = [path] if (!Array.isArray(path)) path = [path]
global.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { global.dispatchDeletePath(path) }, 'Cancel', () => {}) global.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { global.dispatchDeletePath(path) }, 'Cancel', () => { })
} }
const toGist = (path?: string, type?: string) => { const toGist = (path?: string, type?: string) => {
global.dispatchPublishToGist(path, type) global.dispatchPublishToGist(path, type)
} }
const editModeOn = (path: string, type: string, isNew = false) => { const editModeOn = (path: string, type: string, isNew = false) => {
if (global.fs.readonly) return global.toast('Cannot write/modify file system in read only mode.') if (global.fs.readonly) return global.toast('Cannot write/modify file system in read only mode.')
setState(prevState => { setState(prevState => {
@ -547,7 +561,7 @@ export function Workspace () {
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
global.modal(intl.formatMessage({ id: 'filePanel.checkoutGitBranch' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => {}, intl.formatMessage({ id: 'filePanel.cancel' })) global.modal(intl.formatMessage({ id: 'filePanel.checkoutGitBranch' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => { }, intl.formatMessage({ id: 'filePanel.cancel' }))
} }
} }
@ -556,34 +570,34 @@ export function Workspace () {
await global.dispatchCreateNewBranch(branchFilter) await global.dispatchCreateNewBranch(branchFilter)
_paq.push(['trackEvent', 'Workspace', 'GIT', 'switch_to_new_branch']) _paq.push(['trackEvent', 'Workspace', 'GIT', 'switch_to_new_branch'])
} catch (e) { } catch (e) {
global.modal(intl.formatMessage({ id: 'filePanel.checkoutGitBranch' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => {}, intl.formatMessage({ id: 'filePanel.cancel' })) global.modal(intl.formatMessage({ id: 'filePanel.checkoutGitBranch' }), e.message, intl.formatMessage({ id: 'filePanel.ok' }), () => { }, intl.formatMessage({ id: 'filePanel.cancel' }))
} }
} }
const createModalMessage = () => { const createModalMessage = () => {
return ( return (
<> <>
<label id="selectWsTemplate" className="form-check-label" style={{fontWeight: "bolder"}}><FormattedMessage id='filePanel.workspace.chooseTemplate' /></label> <label id="selectWsTemplate" className="form-check-label" style={{ fontWeight: "bolder" }}><FormattedMessage id='filePanel.workspace.chooseTemplate' /></label>
<select name="wstemplate" className="mb-3 form-control custom-select" id="wstemplate" defaultValue='remixDefault' ref={workspaceCreateTemplateInput} onChange={updateWsName}> <select name="wstemplate" className="mb-3 form-control custom-select" id="wstemplate" defaultValue='remixDefault' ref={workspaceCreateTemplateInput} onChange={updateWsName}>
<optgroup style={{fontSize: "medium"}} label="General"> <optgroup style={{ fontSize: "medium" }} label="General">
<option style={{fontSize: "small"}} value='remixDefault'>Basic</option> <option style={{ fontSize: "small" }} value='remixDefault'>Basic</option>
<option style={{fontSize: "small"}} value='blank'>Blank</option> <option style={{ fontSize: "small" }} value='blank'>Blank</option>
</optgroup> </optgroup>
<optgroup style={{fontSize: "medium"}} label="OpenZeppelin"> <optgroup style={{ fontSize: "medium" }} label="OpenZeppelin">
<option style={{fontSize: "small"}} value='ozerc20'>ERC20</option> <option style={{ fontSize: "small" }} value='ozerc20'>ERC20</option>
<option style={{fontSize: "small"}} value='ozerc721'>ERC721</option> <option style={{ fontSize: "small" }} value='ozerc721'>ERC721</option>
<option style={{fontSize: "small"}} value='ozerc1155'>ERC1155</option> <option style={{ fontSize: "small" }} value='ozerc1155'>ERC1155</option>
</optgroup> </optgroup>
<optgroup style={{fontSize: "medium"}} label="0xProject"> <optgroup style={{ fontSize: "medium" }} label="0xProject">
<option style={{fontSize: "small"}} value='zeroxErc20'>ERC20</option> <option style={{ fontSize: "small" }} value='zeroxErc20'>ERC20</option>
</optgroup> </optgroup>
<optgroup style={{fontSize: "medium"}} label="GnosisSafe"> <optgroup style={{ fontSize: "medium" }} label="GnosisSafe">
<option style={{fontSize: "small"}} value='gnosisSafeMultisig'>MultiSig Wallet</option> <option style={{ fontSize: "small" }} value='gnosisSafeMultisig'>MultiSig Wallet</option>
</optgroup> </optgroup>
</select> </select>
<div id="ozcustomization" data-id="ozCustomization" ref={displayOzCustomRef} style={{display: 'none'}} className="mb-2"> <div id="ozcustomization" data-id="ozCustomization" ref={displayOzCustomRef} style={{ display: 'none' }} className="mb-2">
<label className="form-check-label d-block mb-2" style={{fontWeight: "bolder"}}><FormattedMessage id='filePanel.customizeTemplate' /></label> <label className="form-check-label d-block mb-2" style={{ fontWeight: "bolder" }}><FormattedMessage id='filePanel.customizeTemplate' /></label>
<label id="wsName" className="form-check-label d-block mb-1"><FormattedMessage id='filePanel.features' /></label> <label id="wsName" className="form-check-label d-block mb-1"><FormattedMessage id='filePanel.features' /></label>
<div className="mb-2"> <div className="mb-2">
@ -615,7 +629,7 @@ export function Workspace () {
</div> </div>
<label id="wsName" className="form-check-label" style={{fontWeight: "bolder"}} ><FormattedMessage id='filePanel.workspaceName' /></label> <label id="wsName" className="form-check-label" style={{ fontWeight: "bolder" }} ><FormattedMessage id='filePanel.workspaceName' /></label>
<input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={global.plugin.getAvailableWorkspaceName(TEMPLATE_NAMES['remixDefault'])} ref={workspaceCreateInput} className="form-control" /> <input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={global.plugin.getAvailableWorkspaceName(TEMPLATE_NAMES['remixDefault'])} ref={workspaceCreateInput} className="form-control" />
<div className="d-flex py-2 align-items-center custom-control custom-checkbox"> <div className="d-flex py-2 align-items-center custom-control custom-checkbox">
@ -626,7 +640,7 @@ export function Workspace () {
className="form-check-input custom-control-input" className="form-check-input custom-control-input"
type="checkbox" type="checkbox"
disabled={!global.fs.gitConfig.username || !global.fs.gitConfig.email} disabled={!global.fs.gitConfig.username || !global.fs.gitConfig.email}
onChange={() => {}} onChange={() => { }}
/> />
<label <label
htmlFor="initGitRepository" htmlFor="initGitRepository"
@ -640,7 +654,7 @@ export function Workspace () {
{!global.fs.gitConfig.username || !global.fs.gitConfig.email ? {!global.fs.gitConfig.username || !global.fs.gitConfig.email ?
( (
<div className='text-warning'><FormattedMessage id='filePanel.initGitRepositoryWarning' /></div>) <div className='text-warning'><FormattedMessage id='filePanel.initGitRepositoryWarning' /></div>)
:<></> : <></>
} }
</> </>
@ -650,7 +664,7 @@ export function Workspace () {
const renameModalMessage = () => { const renameModalMessage = () => {
return ( return (
<> <>
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ currentWorkspace } ref={workspaceRenameInput} className="form-control" /> <input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={currentWorkspace} ref={workspaceRenameInput} className="form-control" />
</> </>
) )
} }
@ -675,7 +689,7 @@ export function Workspace () {
return ( return (
<div className='d-flex flex-column justify-content-between h-100'> <div className='d-flex flex-column justify-content-between h-100'>
<div className='remixui_container overflow-auto' style={{ maxHeight: selectedWorkspace && selectedWorkspace.isGitRepo ? '95%' : '100%' }} onContextMenu={(e)=>{ <div className='remixui_container overflow-auto' style={{ maxHeight: selectedWorkspace && selectedWorkspace.isGitRepo ? '95%' : '100%' }} onContextMenu={(e) => {
e.preventDefault() e.preventDefault()
handleContextMenu(e.pageX, e.pageY, ROOT_PATH, "workspace", 'workspace') handleContextMenu(e.pageX, e.pageY, ROOT_PATH, "workspace", 'workspace')
} }
@ -683,9 +697,35 @@ export function Workspace () {
<div className='d-flex flex-column w-100 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}> <div className='d-flex flex-column w-100 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div> <div>
<header> <header>
<div className="mx-2 my-2 d-flex flex-column"> <div className="mx-2 mb-2 d-flex flex-column">
<div className="d-flex"> <div className="d-flex justify-content-between">
{currentWorkspace !== LOCALHOST ? (<span className="remixui_topmenu d-flex"> {!isElectron() ?
<span className="d-flex align-items-end">
<label className="pl-1 form-check-label" htmlFor="workspacesSelect" style={{ wordBreak: 'keep-all' }}>
<FormattedMessage id='filePanel.workspace' />
</label>
</span> : null}
{currentWorkspace !== LOCALHOST && !isElectron() ? (<span className="remixui_menu remixui_topmenu d-flex justify-content-between align-items-end w-75">
<CustomTooltip
placement="top"
tooltipId="createWorkspaceTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.create' />}
>
<span
hidden={currentWorkspace === LOCALHOST}
id='workspaceCreate'
data-id='workspaceCreate'
onClick={(e) => {
e.stopPropagation()
createWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceCreate'])
}}
style={{ fontSize: 'medium' }}
className='far fa-plus remixui_menuicon d-flex align-self-end'
>
</span>
</CustomTooltip>
<Dropdown id="workspacesMenuDropdown" data-id="workspacesMenuDropdown" onToggle={() => hideIconsMenu(!showIconsMenu)} show={showIconsMenu}> <Dropdown id="workspacesMenuDropdown" data-id="workspacesMenuDropdown" onToggle={() => hideIconsMenu(!showIconsMenu)} show={showIconsMenu}>
<Dropdown.Toggle <Dropdown.Toggle
as={CustomIconsToggle} as={CustomIconsToggle}
@ -710,18 +750,14 @@ export function Workspace () {
addHelperScripts={addHelperScripts} addHelperScripts={addHelperScripts}
addTsSolTestGithubAction={addTsSolTestGithubAction} addTsSolTestGithubAction={addTsSolTestGithubAction}
showIconsMenu={showIconsMenu} showIconsMenu={showIconsMenu}
hideWorkspaceOptions={ currentWorkspace === LOCALHOST } hideWorkspaceOptions={currentWorkspace === LOCALHOST}
hideLocalhostOptions={ currentWorkspace === NO_WORKSPACE } hideLocalhostOptions={currentWorkspace === NO_WORKSPACE}
/> />
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
</span>) : null} </span>) : null}
<span className="d-flex">
<label className="pl-1 form-check-label" htmlFor="workspacesSelect" style={{wordBreak: 'keep-all'}}>
<FormattedMessage id='filePanel.workspace' />
</label>
</span>
</div> </div>
{!isElectron() ? (
<Dropdown id="workspacesSelect" data-id="workspacesSelect" onToggle={toggleDropdown} show={showDropdown}> <Dropdown id="workspacesSelect" data-id="workspacesSelect" onToggle={toggleDropdown} show={showDropdown}>
<Dropdown.Toggle <Dropdown.Toggle
as={CustomToggle} as={CustomToggle}
@ -729,7 +765,7 @@ export function Workspace () {
className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control mt-1" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control mt-1"
icon={selectedWorkspace && selectedWorkspace.isGitRepo && !(currentWorkspace === LOCALHOST) ? 'far fa-code-branch' : null} icon={selectedWorkspace && selectedWorkspace.isGitRepo && !(currentWorkspace === LOCALHOST) ? 'far fa-code-branch' : null}
> >
{ selectedWorkspace ? selectedWorkspace.name : currentWorkspace === LOCALHOST ? formatNameForReadonly("localhost") : NO_WORKSPACE } {selectedWorkspace ? selectedWorkspace.name : currentWorkspace === LOCALHOST ? formatNameForReadonly("localhost") : NO_WORKSPACE}
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className='w-100 custom-dropdown-items' data-id="custom-dropdown-items"> <Dropdown.Menu as={CustomMenu} className='w-100 custom-dropdown-items' data-id="custom-dropdown-items">
@ -742,7 +778,8 @@ export function Workspace () {
<span className="pl-3"> - create a new workspace - </span> <span className="pl-3"> - create a new workspace - </span>
} }
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item onClick={() => { switchWorkspace(LOCALHOST) }}>{currentWorkspace === LOCALHOST ? <span>&#10003; localhost </span> : <span className="pl-3"> { LOCALHOST } </span>}</Dropdown.Item> <Dropdown.Item onClick={() => { switchWorkspace(LOCALHOST) }}>{currentWorkspace === LOCALHOST ? <span>&#10003; localhost </span> : <span className="pl-3"> {LOCALHOST} </span>}</Dropdown.Item>
<Dropdown.Item onClick={() => { switchWorkspace(ELECTRON) }}>{currentWorkspace === ELECTRON ? <span>&#10003; electron </span> : <span className="pl-3"> {ELECTRON} </span>}</Dropdown.Item>
{ {
global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => ( global.fs.browser.workspaces.map(({ name, isGitRepo }, index) => (
<Dropdown.Item <Dropdown.Item
@ -752,32 +789,34 @@ export function Workspace () {
}} }}
data-id={`dropdown-item-${name}`} data-id={`dropdown-item-${name}`}
> >
{ isGitRepo ? {isGitRepo ?
<div className='d-flex justify-content-between'> <div className='d-flex justify-content-between'>
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span> <span>{currentWorkspace === name ? <span>&#10003; {name} </span> : <span className="pl-3">{name}</span>}</span>
<i className='fas fa-code-branch pt-1'></i> <i className='fas fa-code-branch pt-1'></i>
</div> : </div> :
<span>{ currentWorkspace === name ? <span>&#10003; { name } </span> : <span className="pl-3">{ name }</span> }</span> <span>{currentWorkspace === name ? <span>&#10003; {name} </span> : <span className="pl-3">{name}</span>}</span>
} }
</Dropdown.Item> </Dropdown.Item>
)) ))
} }
{ ((global.fs.browser.workspaces.length <= 0) || currentWorkspace === NO_WORKSPACE) && <Dropdown.Item onClick={() => { switchWorkspace(NO_WORKSPACE) }}>{ <span className="pl-3">NO_WORKSPACE</span> }</Dropdown.Item> } {((global.fs.browser.workspaces.length <= 0) || currentWorkspace === NO_WORKSPACE) && <Dropdown.Item onClick={() => { switchWorkspace(NO_WORKSPACE) }}>{<span className="pl-3">NO_WORKSPACE</span>}</Dropdown.Item>}
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
) : null}
</div> </div>
</header> </header>
</div> </div>
<ElectronMenu></ElectronMenu>
<div className='h-100 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}> <div className='h-100 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}>
<div className='h-100'> <div className='h-100'>
{ (global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>} {(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>}
{ !(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && {!(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) &&
(global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) && (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) && (!isElectron() || global.fs.browser.isSuccessfulWorkspace) &&
<div className='h-100 remixui_treeview' data-id='filePanelFileExplorerTree'> <div className='h-100 remixui_treeview' data-id='filePanelFileExplorerTree'>
<FileExplorer <FileExplorer
fileState={global.fs.browser.fileState} fileState={global.fs.browser.fileState}
name={currentWorkspace} name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '']} menuItems={['createNewFile', 'createNewFolder', !isElectron() ? 'publishToGist':'', canUpload && !isElectron() ? 'uploadFile' : '', canUpload && !isElectron() ? 'uploadFolder' : '']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems} contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems} removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files} files={global.fs.browser.files}
@ -825,8 +864,9 @@ export function Workspace () {
/> />
</div> </div>
} }
{ global.fs.localhost.isRequestingLocalhost && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> }
{ (global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) && {global.fs.localhost.isRequestingLocalhost && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>}
{(global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) &&
<div className='h-100 filesystemexplorer remixui_treeview'> <div className='h-100 filesystemexplorer remixui_treeview'>
<FileExplorer <FileExplorer
name='localhost' name='localhost'
@ -882,6 +922,7 @@ export function Workspace () {
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{ {
selectedWorkspace && selectedWorkspace &&
@ -891,7 +932,7 @@ export function Workspace () {
<div className="pt-1 mr-1" data-id="workspaceGitBranchesDropdown"> <div className="pt-1 mr-1" data-id="workspaceGitBranchesDropdown">
<Dropdown style={{ height: 30, minWidth: 80 }} onToggle={toggleBranches} show={showBranches} drop={'up'}> <Dropdown style={{ height: 30, minWidth: 80 }} onToggle={toggleBranches} show={showBranches} drop={'up'}>
<Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control h-100 p-0 pl-2 pr-2 text-dark" icon={null}> <Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control h-100 p-0 pl-2 pr-2 text-dark" icon={null}>
{ global.fs.browser.isRequestingCloning ? <i className="fad fa-spinner fa-spin"></i> : currentBranch || '-none-' } {global.fs.browser.isRequestingCloning ? <i className="fad fa-spinner fa-spin"></i> : currentBranch || '-none-'}
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} className='custom-dropdown-items branches-dropdown'> <Dropdown.Menu as={CustomMenu} className='custom-dropdown-items branches-dropdown'>
@ -915,11 +956,11 @@ export function Workspace () {
filteredBranches.length > 0 ? filteredBranches.map((branch, index) => { filteredBranches.length > 0 ? filteredBranches.map((branch, index) => {
return ( return (
<Dropdown.Item key={index} onClick={() => { switchToBranch(branch) }} title={branch.remote ? 'Checkout new branch from remote branch' : 'Checkout to local branch'}> <Dropdown.Item key={index} onClick={() => { switchToBranch(branch) }} title={branch.remote ? 'Checkout new branch from remote branch' : 'Checkout to local branch'}>
<div data-id={`workspaceGit-${ branch.remote ? `${branch.remote}/${branch.name}` : branch.name }`}> <div data-id={`workspaceGit-${branch.remote ? `${branch.remote}/${branch.name}` : branch.name}`}>
{ {
(currentBranch === branch.name) && !branch.remote ? (currentBranch === branch.name) && !branch.remote ?
<span>&#10003; <i className='far fa-code-branch'></i><span className='pl-1'>{ branch.name }</span></span> : <span>&#10003; <i className='far fa-code-branch'></i><span className='pl-1'>{branch.name}</span></span> :
<span className='pl-3'><i className={`far ${ branch.remote ? 'fa-cloud' : 'fa-code-branch'}`}></i><span className='pl-1'>{ branch.remote ? `${branch.remote}/${branch.name}` : branch.name }</span></span> <span className='pl-3'><i className={`far ${branch.remote ? 'fa-cloud' : 'fa-code-branch'}`}></i><span className='pl-1'>{branch.remote ? `${branch.remote}/${branch.name}` : branch.name}</span></span>
} }
</div> </div>
</Dropdown.Item> </Dropdown.Item>
@ -927,7 +968,7 @@ export function Workspace () {
}) : }) :
<Dropdown.Item onClick={switchToNewBranch}> <Dropdown.Item onClick={switchToNewBranch}>
<div className="pl-1 pr-1" data-id="workspaceGitCreateNewBranch"> <div className="pl-1 pr-1" data-id="workspaceGitCreateNewBranch">
<i className="fas fa-code-branch pr-2"></i><span><FormattedMessage id='filePanel.createBranch' />: { branchFilter } from '{currentBranch}'</span> <i className="fas fa-code-branch pr-2"></i><span><FormattedMessage id='filePanel.createBranch' />: {branchFilter} from '{currentBranch}'</span>
</div> </div>
</Dropdown.Item> </Dropdown.Item>
} }

@ -5,7 +5,7 @@ import { fileDecoration } from '@remix-ui/file-decorators'
import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types' import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types'
import { ViewPlugin } from '@remixproject/engine-web' import { ViewPlugin } from '@remixproject/engine-web'
export type action = { name: string, type?: Array<'folder' | 'gist' | 'file' | 'workspace'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean, group: number } export type action = { name: string, type?: Array<'folder' | 'gist' | 'file' | 'workspace'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean, group: number, platform?: 'electron' | 'browser' }
export interface JSONStandardInput { export interface JSONStandardInput {
language: "Solidity"; language: "Solidity";
settings?: any, settings?: any,

@ -62,7 +62,8 @@ export const contextMenuActions: MenuItems = [{
type: ['file', 'folder', 'workspace'], type: ['file', 'folder', 'workspace'],
multiselect: false, multiselect: false,
label: '', label: '',
group: 2 group: 2,
platform: 'browser'
}, { }, {
id: 'run', id: 'run',
name: 'Run', name: 'Run',
@ -76,35 +77,40 @@ export const contextMenuActions: MenuItems = [{
type: ['gist'], type: ['gist'],
multiselect: false, multiselect: false,
label: '', label: '',
group: 4 group: 4,
platform: 'browser'
}, { }, {
id: 'publishFolderToGist', id: 'publishFolderToGist',
name: 'Publish folder to gist', name: 'Publish folder to gist',
type: ['folder'], type: ['folder'],
multiselect: false, multiselect: false,
label: '', label: '',
group: 4 group: 4,
platform: 'browser'
}, { }, {
id: 'publishFileToGist', id: 'publishFileToGist',
name: 'Publish file to gist', name: 'Publish file to gist',
type: ['file'], type: ['file'],
multiselect: false, multiselect: false,
label: '', label: '',
group: 4 group: 4,
platform: 'browser'
}, { }, {
id: 'uploadFile', id: 'uploadFile',
name: 'Load a Local File', name: 'Load a Local File',
type: ['folder', 'gist', 'workspace'], type: ['folder', 'gist', 'workspace'],
multiselect: false, multiselect: false,
label: 'Load a Local File', label: 'Load a Local File',
group: 4 group: 4,
platform: 'browser'
}, { }, {
id: 'publishToGist', id: 'publishToGist',
name: 'Push changes to gist', name: 'Push changes to gist',
type: ['folder', 'gist'], type: ['folder', 'gist'],
multiselect: false, multiselect: false,
label: 'Publish all to Gist', label: 'Publish all to Gist',
group: 4 group: 4,
platform: 'browser'
}, },
{ {
id: 'publishWorkspace', id: 'publishWorkspace',
@ -112,5 +118,6 @@ export const contextMenuActions: MenuItems = [{
type: ['workspace'], type: ['workspace'],
multiselect: false, multiselect: false,
label: '', label: '',
group: 4 group: 4,
platform: 'browser'
}] }]

@ -0,0 +1,2 @@
export * from './lib/components/remix-ui-xterm'
export * from './lib/components/remix-ui-xterminals'

@ -0,0 +1,48 @@
import React, { useState, useEffect, forwardRef } from 'react' // eslint-disable-line
import { ElectronPlugin } from '@remixproject/engine-electron'
import { Xterm } from './xterm-wrap'
import { FitAddon } from './xterm-fit-addOn';
const fitAddon = new FitAddon()
export interface RemixUiXtermProps {
plugin: ElectronPlugin
pid: number
send: (data: string, pid: number) => void
timeStamp: number
setTerminalRef: (pid: number, ref: any) => void
theme: {
backgroundColor: string
textColor: string
}
}
const RemixUiXterm = (props: RemixUiXtermProps) => {
const { plugin, pid, send, timeStamp } = props
const xtermRef = React.useRef(null)
useEffect(() => {
props.setTerminalRef(pid, xtermRef.current)
}, [xtermRef.current])
const onKey = (event: { key: string; domEvent: KeyboardEvent }) => {
send(event.key, pid)
}
return (
<Xterm
addons={[fitAddon]}
options={{ theme: { background: props.theme.backgroundColor, foreground: props.theme.textColor } }}
onRender={() => fitAddon.fit()}
ref={xtermRef}
onKey={onKey}></Xterm>
)
}
export default RemixUiXterm

@ -0,0 +1,234 @@
import React, { useState, useEffect } from 'react' // eslint-disable-line
import { ElectronPlugin } from '@remixproject/engine-electron'
import RemixUiXterm from './remix-ui-xterm'
import '../css/index.css'
import { Button, ButtonGroup, Dropdown, Tab, Tabs } from 'react-bootstrap'
import { CustomIconsToggle } from '@remix-ui/helper'
import { RemixUiTerminal } from '@remix-ui/terminal'
export interface RemixUiXterminalsProps {
plugin: ElectronPlugin
onReady: (api: any) => void
}
export interface xtermState {
pid: number
queue: string
timeStamp: number
ref: any
hidden: boolean
}
export const RemixUiXterminals = (props: RemixUiXterminalsProps) => {
const [terminals, setTerminals] = useState<xtermState[]>([])
const [workingDir, setWorkingDir] = useState<string>('')
const [showOutput, setShowOutput] = useState<boolean>(true)
const [theme, setTheme] = useState<any>(themeCollection[0])
const [terminalsEnabled, setTerminalsEnabled] = useState<boolean>(false)
const [shells, setShells] = useState<string[]>([])
const { plugin } = props
useEffect(() => {
setTimeout(async () => {
plugin.on('xterm', 'loaded', async () => {
console.log('xterm loaded')
})
plugin.on('xterm', 'data', async (data: string, pid: number) => {
writeToTerminal(data, pid)
})
plugin.on('xterm', 'close', async (pid: number) => {
setTerminals(prevState => {
const removed = prevState.filter(xtermState => xtermState.pid !== pid)
if (removed.length > 0)
removed[removed.length - 1].hidden = false
if(removed.length === 0)
setShowOutput(true)
return [...removed]
})
})
plugin.on('fs', 'workingDirChanged', (path: string) => {
setWorkingDir(path)
setTerminalsEnabled(true)
})
plugin.on('theme', 'themeChanged', async (theme) => {
handleThemeChange(theme)
})
const theme = await plugin.call('theme', 'currentTheme')
handleThemeChange(theme)
const shells = await plugin.call('xterm', 'getShells')
setShells(shells)
}, 2000)
}, [])
const handleThemeChange = (theme: any) => {
themeCollection.forEach((themeItem) => {
if (themeItem.themeName === theme.name) {
setTheme(themeItem)
}
})
}
const writeToTerminal = (data: string, pid: number) => {
setTerminals(prevState => {
const terminal = prevState.find(xtermState => xtermState.pid === pid)
if (terminal.ref && terminal.ref.terminal) {
terminal.ref.terminal.write(data)
} else {
terminal.queue += data
}
return [...prevState]
})
}
const send = (data: string, pid: number) => {
plugin.call('xterm', 'keystroke', data, pid)
}
const createTerminal = async (shell?: string) => {
const pid = await plugin.call('xterm', 'createTerminal', workingDir, shell)
setShowOutput(false)
setTerminals(prevState => {
// set all to hidden
prevState.forEach(xtermState => {
xtermState.hidden = true
})
return [...prevState, {
pid: pid,
queue: '',
timeStamp: Date.now(),
ref: null,
hidden: false
}]
})
}
const setTerminalRef = (pid: number, ref: any) => {
setTerminals(prevState => {
const terminal = prevState.find(xtermState => xtermState.pid === pid)
terminal.ref = ref
if (terminal.queue) {
ref.terminal.write(terminal.queue)
terminal.queue = ''
}
return [...prevState]
})
}
const selectTerminal = (state: xtermState) => {
setTerminals(prevState => {
// set all to hidden
prevState.forEach(xtermState => {
xtermState.hidden = true
})
const terminal = prevState.find(xtermState => xtermState.pid === state.pid)
terminal.hidden = false
return [...prevState]
})
}
const closeTerminal = () => {
const pid = terminals.find(xtermState => xtermState.hidden === false).pid
if (pid)
plugin.call('xterm', 'close', pid)
}
const selectOutput = () => {
setShowOutput(true)
}
const showTerminal = () => {
setShowOutput(false)
if (terminals.length === 0) createTerminal()
}
return (<>
<div className='xterm-panel'>
<div className='xterm-panel-header bg-light'>
<div className='xterm-panel-header-left p-1'>
<button className={`btn btn-sm btn-secondary mr-2 ${!showOutput ? 'xterm-btn-none' : 'xterm-btn-active'}`} onClick={selectOutput}>ouput</button>
<button className={`btn btn-sm btn-secondary ${terminalsEnabled ? '' : 'd-none'} ${showOutput ? 'xterm-btn-none' : 'xterm-btn-active'}`} onClick={showTerminal}><span className="far fa-terminal border-0 ml-1"></span></button>
</div>
<div className={`xterm-panel-header-right ${showOutput ? 'd-none' : ''}`}>
<Dropdown as={ButtonGroup}>
<button className="btn btn-sm btn-secondary" onClick={async() => createTerminal()}><span className="far fa-plus border-0 p-0 m-0"></span></button>
<Dropdown.Toggle split variant="secondary" id="dropdown-split-basic" />
<Dropdown.Menu className='custom-dropdown-items remixui_menuwidth'>
{shells.map((shell, index) => {
return (<Dropdown.Item key={index} onClick={async() => await createTerminal(shell)}>{shell}</Dropdown.Item>)
})}
</Dropdown.Menu>
</Dropdown>
<button className="btn ml-2 btn-sm btn-secondary" onClick={closeTerminal}><span className="far fa-trash border-0 ml-1"></span></button>
</div>
</div>
<div className='remix-ui-xterminals-container'>
<>
<div className={`${!showOutput ? 'd-none' : 'd-block w-100'} `}>
<RemixUiTerminal
plugin={props.plugin}
onReady={props.onReady} />
</div>
<div className={`remix-ui-xterminals-section ${showOutput ? 'd-none' : 'd-flex'} `}>
{terminals.map((xtermState) => {
return (
<div className={`h-100 xterm-terminal ${xtermState.hidden ? 'hide-xterm' : 'show-xterm'}`} key={xtermState.pid} data-id={`remixUIXT${xtermState.pid}`}>
<RemixUiXterm theme={theme} setTerminalRef={setTerminalRef} timeStamp={xtermState.timeStamp} send={send} pid={xtermState.pid} plugin={plugin}></RemixUiXterm>
</div>
)
})}
<div className='remix-ui-xterminals-buttons border-left'>
{terminals.map((xtermState, index) => {
return (<button key={index} onClick={async () => selectTerminal(xtermState)} className={`btn btn-sm mt-2 btn-secondary ${xtermState.hidden ? 'xterm-btn-none' : 'xterm-btn-active'}`}><span className="fa fa-terminal border-0 p-0 m-0"></span></button>)
})}
</div>
</div>
</>
</div>
</div>
</>)
}
const themeCollection = [
{ themeName: 'HackerOwl', backgroundColor: '#011628', textColor: '#babbcc',
shapeColor: '#8694a1',fillColor: '#011C32'},
{ themeName: 'Cerulean', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#343a40',fillColor: '#f8f9fa'},
{ themeName: 'Cyborg', backgroundColor: '#060606', textColor: '#adafae',
shapeColor: '#adafae', fillColor: '#222222'},
{ themeName: 'Dark', backgroundColor: '#222336', textColor: '#babbcc',
shapeColor: '#babbcc',fillColor: '#2a2c3f'},
{ themeName: 'Flatly', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#7b8a8b',fillColor: '#ffffff'},
{ themeName: 'Black', backgroundColor: '#1a1a1a', textColor: '#babbcc',
shapeColor: '#b5b4bc',fillColor: '#1f2020'},
{ themeName: 'Light', backgroundColor: '#eef1f6', textColor: '#3b445e',
shapeColor: '#343a40',fillColor: '#ffffff'},
{ themeName: 'Midcentury', backgroundColor: '#DBE2E0', textColor: '#11556c',
shapeColor: '#343a40',fillColor: '#eeede9'},
{ themeName: 'Spacelab', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#333333', fillColor: '#eeeeee'},
{ themeName: 'Candy', backgroundColor: '#d5efff', textColor: '#11556c',
shapeColor: '#343a40',fillColor: '#fbe7f8' },
{ themeName: 'Violet', backgroundColor: '#f1eef6', textColor: '#3b445e',
shapeColor: '#343a40',fillColor: '#f8fafe' },
{ themeName: 'Pride', backgroundColor: '#f1eef6', textColor: '#343a40',
shapeColor: '#343a40',fillColor: '#f8fafe' },
]

@ -0,0 +1,92 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal, ITerminalAddon } from 'xterm';
interface ITerminalDimensions {
/**
* The number of rows in the terminal.
*/
rows: number;
/**
* The number of columns in the terminal.
*/
cols: number;
}
const MINIMUM_COLS = 2;
const MINIMUM_ROWS = 1;
export class FitAddon implements ITerminalAddon {
private _terminal: Terminal | undefined;
constructor() {}
public activate(terminal: Terminal): void {
this._terminal = terminal;
}
public dispose(): void {}
public fit(): void {
const dims = this.proposeDimensions();
if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) {
return;
}
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
// Force a full render
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
core._renderService.clear();
this._terminal.resize(dims.cols, dims.rows);
}
}
public proposeDimensions(): ITerminalDimensions | undefined {
if (!this._terminal) {
return undefined;
}
if (!this._terminal.element || !this._terminal.element.parentElement) {
return undefined;
}
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
const dims = core._renderService.dimensions;
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
return undefined;
}
const scrollbarWidth = this._terminal.options.scrollback === 0 ?
0 : core.viewport.scrollBarWidth;
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
const elementStyle = window.getComputedStyle(this._terminal.element);
const elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
};
return geometry;
}
}

@ -0,0 +1,237 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import 'xterm/css/xterm.css'
// We are using these as types.
// eslint-disable-next-line no-unused-vars
import { Terminal, ITerminalOptions, ITerminalAddon } from 'xterm'
interface IProps {
/**
* Class name to add to the terminal container.
*/
className?: string
/**
* Options to initialize the terminal with.
*/
options?: ITerminalOptions
/**
* An array of XTerm addons to load along with the terminal.
*/
addons?: Array<ITerminalAddon>
/**
* Adds an event listener for when a binary event fires. This is used to
* enable non UTF-8 conformant binary messages to be sent to the backend.
* Currently this is only used for a certain type of mouse reports that
* happen to be not UTF-8 compatible.
* The event value is a JS string, pass it to the underlying pty as
* binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`.
*/
onBinary?(data: string): void
/**
* Adds an event listener for the cursor moves.
*/
onCursorMove?(): void
/**
* Adds an event listener for when a data event fires. This happens for
* example when the user types or pastes into the terminal. The event value
* is whatever `string` results, in a typical setup, this should be passed
* on to the backing pty.
*/
onData?(data: string): void
/**
* Adds an event listener for when a key is pressed. The event value contains the
* string that will be sent in the data event as well as the DOM event that
* triggered it.
*/
onKey?(event: { key: string; domEvent: KeyboardEvent }): void
/**
* Adds an event listener for when a line feed is added.
*/
onLineFeed?(): void
/**
* Adds an event listener for when a scroll occurs. The event value is the
* new position of the viewport.
* @returns an `IDisposable` to stop listening.
*/
onScroll?(newPosition: number): void
/**
* Adds an event listener for when a selection change occurs.
*/
onSelectionChange?(): void
/**
* Adds an event listener for when rows are rendered. The event value
* contains the start row and end rows of the rendered area (ranges from `0`
* to `Terminal.rows - 1`).
*/
onRender?(event: { start: number; end: number }): void
/**
* Adds an event listener for when the terminal is resized. The event value
* contains the new size.
*/
onResize?(event: { cols: number; rows: number }): void
/**
* Adds an event listener for when an OSC 0 or OSC 2 title change occurs.
* The event value is the new title.
*/
onTitleChange?(newTitle: string): void
/**
* Attaches a custom key event handler which is run before keys are
* processed, giving consumers of xterm.js ultimate control as to what keys
* should be processed by the terminal and what keys should not.
*
* @param event The custom KeyboardEvent handler to attach.
* This is a function that takes a KeyboardEvent, allowing consumers to stop
* propagation and/or prevent the default action. The function returns
* whether the event should be processed by xterm.js.
*/
customKeyEventHandler?(event: KeyboardEvent): boolean
}
export class Xterm extends React.Component<IProps> {
/**
* The ref for the containing element.
*/
terminalRef: React.RefObject<HTMLDivElement>
/**
* XTerm.js Terminal object.
*/
terminal!: Terminal // This is assigned in the setupTerminal() which is called from the constructor
static propTypes = {
className: PropTypes.string,
options: PropTypes.object,
addons: PropTypes.array,
onBinary: PropTypes.func,
onCursorMove: PropTypes.func,
onData: PropTypes.func,
onKey: PropTypes.func,
onLineFeed: PropTypes.func,
onScroll: PropTypes.func,
onSelectionChange: PropTypes.func,
onRender: PropTypes.func,
onResize: PropTypes.func,
onTitleChange: PropTypes.func,
customKeyEventHandler: PropTypes.func,
}
constructor(props: IProps) {
super(props)
this.terminalRef = React.createRef()
// Bind Methods
this.onData = this.onData.bind(this)
this.onCursorMove = this.onCursorMove.bind(this)
this.onKey = this.onKey.bind(this)
this.onBinary = this.onBinary.bind(this)
this.onLineFeed = this.onLineFeed.bind(this)
this.onScroll = this.onScroll.bind(this)
this.onSelectionChange = this.onSelectionChange.bind(this)
this.onRender = this.onRender.bind(this)
this.onResize = this.onResize.bind(this)
this.onTitleChange = this.onTitleChange.bind(this)
this.setupTerminal()
}
setupTerminal() {
// Setup the XTerm terminal.
this.terminal = new Terminal(this.props.options)
// Load addons if the prop exists.
if (this.props.addons) {
this.props.addons.forEach((addon) => {
this.terminal.loadAddon(addon)
})
}
// Create Listeners
this.terminal.onBinary(this.onBinary)
this.terminal.onCursorMove(this.onCursorMove)
this.terminal.onData(this.onData)
this.terminal.onKey(this.onKey)
this.terminal.onLineFeed(this.onLineFeed)
this.terminal.onScroll(this.onScroll)
this.terminal.onSelectionChange(this.onSelectionChange)
this.terminal.onRender(this.onRender)
this.terminal.onResize(this.onResize)
this.terminal.onTitleChange(this.onTitleChange)
// Add Custom Key Event Handler
if (this.props.customKeyEventHandler) {
this.terminal.attachCustomKeyEventHandler(this.props.customKeyEventHandler)
}
}
componentDidMount() {
if (this.terminalRef.current) {
// Creates the terminal within the container element.
this.terminal.open(this.terminalRef.current)
}
}
componentWillUnmount() {
// When the component unmounts dispose of the terminal and all of its listeners.
this.terminal.dispose()
}
private onBinary(data: string) {
if (this.props.onBinary) this.props.onBinary(data)
}
private onCursorMove() {
if (this.props.onCursorMove) this.props.onCursorMove()
}
private onData(data: string) {
if (this.props.onData) this.props.onData(data)
}
private onKey(event: { key: string; domEvent: KeyboardEvent }) {
if (this.props.onKey) this.props.onKey(event)
}
private onLineFeed() {
if (this.props.onLineFeed) this.props.onLineFeed()
}
private onScroll(newPosition: number) {
if (this.props.onScroll) this.props.onScroll(newPosition)
}
private onSelectionChange() {
if (this.props.onSelectionChange) this.props.onSelectionChange()
}
private onRender(event: { start: number; end: number }) {
if (this.props.onRender) this.props.onRender(event)
}
private onResize(event: { cols: number; rows: number }) {
if (this.props.onResize) this.props.onResize(event)
}
private onTitleChange(newTitle: string) {
if (this.props.onTitleChange) this.props.onTitleChange(newTitle)
}
render() {
return <div className={this.props.className} ref={this.terminalRef} />
}
}

@ -0,0 +1,66 @@
.remix-ui-xterminals-container {
display: flex;
flex-direction: row;
}
.xterm-panel {
}
.remix-ui-xterminals-buttons {
display: flex;
flex-direction: column;
}
.hide-xterm{
display: none;
}
.show-xterm{
display: block;
}
.xterm-btn-active {
background-color: var(--primary);
}
.xterm-btn-none {
background-color: var(--secondary);
}
.xterm-terminal {
flex-grow: 1;
height: 100%;
width: 100%;
}
.xterm-panel-header-right {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-self: flex-end;
}
.xterm-panel-header {
display: flex;
flex-direction: row;
}
.xterm-panel-header-left {
display: flex;
flex-direction: row;
flex-grow: 1;
}
.remix-ui-xterminals-section {
display: flex;
flex-direction: row;
width: 100%;
z-index: 3;
}
.hide-terminals {
width: 0;
}
.show-terminals {
width: 100%;
}

@ -3,6 +3,7 @@
"version": "0.34.1-dev", "version": "0.34.1-dev",
"license": "MIT", "license": "MIT",
"description": "Ethereum Remix Monorepo", "description": "Ethereum Remix Monorepo",
"main": "index.js",
"keywords": [ "keywords": [
"ethereum", "ethereum",
"solidity", "solidity",
@ -52,6 +53,7 @@
"publish:libs": "yarn run build:libs && lerna publish --skip-git && yarn run bumpVersion:libs", "publish:libs": "yarn run build:libs && lerna publish --skip-git && yarn run bumpVersion:libs",
"publishDev:libs": "yarn run build:libs && lerna publish --npm-tag alpha --skip-git && yarn run bumpVersion:libs", "publishDev:libs": "yarn run build:libs && lerna publish --npm-tag alpha --skip-git && yarn run bumpVersion:libs",
"build:e2e": "node apps/remix-ide-e2e/src/buildGroupTests.js && tsc -p apps/remix-ide-e2e/tsconfig.e2e.json", "build:e2e": "node apps/remix-ide-e2e/src/buildGroupTests.js && tsc -p apps/remix-ide-e2e/tsconfig.e2e.json",
"build:desktop": "rm -rf apps/remixdesktop/build/remix-ide && mkdir apps/remixdesktop/build && NX_DESKTOP_FROM_DIST=true nx build remix-ide --configuration=desktop && cp -r dist/apps/remix-ide apps/remixdesktop/build/remix-ide",
"babel": "babel", "babel": "babel",
"watch:e2e": "nodemon", "watch:e2e": "nodemon",
"bumpVersion:libs": "gulp & gulp syncLibVersions;", "bumpVersion:libs": "gulp & gulp syncLibVersions;",
@ -130,13 +132,15 @@
"@openzeppelin/contracts": "^4.7.3", "@openzeppelin/contracts": "^4.7.3",
"@openzeppelin/upgrades-core": "^1.22.0", "@openzeppelin/upgrades-core": "^1.22.0",
"@openzeppelin/wizard": "0.2.0", "@openzeppelin/wizard": "0.2.0",
"@remixproject/engine": "0.3.33", "@remixproject/engine": "0.3.37",
"@remixproject/engine-web": "0.3.33", "@remixproject/engine-electron": "0.3.37",
"@remixproject/plugin": "0.3.33", "@remixproject/engine-web": "0.3.37",
"@remixproject/plugin-api": "0.3.33", "@remixproject/plugin": "0.3.37",
"@remixproject/plugin-utils": "0.3.33", "@remixproject/plugin-api": "0.3.37",
"@remixproject/plugin-webview": "0.3.33", "@remixproject/plugin-electron": "0.3.37",
"@remixproject/plugin-ws": "0.3.33", "@remixproject/plugin-utils": "0.3.37",
"@remixproject/plugin-webview": "0.3.37",
"@remixproject/plugin-ws": "0.3.37",
"@types/nightwatch": "^2.3.1", "@types/nightwatch": "^2.3.1",
"@walletconnect/ethereum-provider": "^2.6.2", "@walletconnect/ethereum-provider": "^2.6.2",
"@walletconnect/sign-client": "^2.6.0", "@walletconnect/sign-client": "^2.6.0",
@ -156,6 +160,7 @@
"core-js": "^3.6.5", "core-js": "^3.6.5",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"document-register-element": "1.13.1", "document-register-element": "1.13.1",
"electron-squirrel-startup": "^1.0.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"ethers": "^5", "ethers": "^5",
"ethjs-util": "^0.1.6", "ethjs-util": "^0.1.6",
@ -171,11 +176,12 @@
"http-server": "^14.1.1", "http-server": "^14.1.1",
"intro.js": "^4.1.0", "intro.js": "^4.1.0",
"isbinaryfile": "^3.0.2", "isbinaryfile": "^3.0.2",
"isomorphic-git": "^1.8.2", "isomorphic-git": "^1.24.0",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"jszip": "^3.6.0", "jszip": "^3.6.0",
"just-once": "^2.2.0",
"latest-version": "^5.1.0", "latest-version": "^5.1.0",
"merge": "^2.1.1", "merge": "^2.1.1",
"npm-install-version": "^6.0.2", "npm-install-version": "^6.0.2",
@ -211,7 +217,9 @@
"wagmi": "^0.12.7", "wagmi": "^0.12.7",
"web3": "^1.8.0", "web3": "^1.8.0",
"winston": "^3.3.3", "winston": "^3.3.3",
"ws": "^7.3.0" "ws": "^7.3.0",
"xterm": "^5.2.1",
"xterm-addon-search": "^0.12.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.19.3", "@babel/cli": "^7.19.3",
@ -227,15 +235,16 @@
"@babel/preset-stage-0": "^7.0.0", "@babel/preset-stage-0": "^7.0.0",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.18.6",
"@babel/register": "^7.4.4", "@babel/register": "^7.4.4",
"@electron-forge/cli": "^6.1.1",
"@fortawesome/fontawesome-free": "^5.8.1", "@fortawesome/fontawesome-free": "^5.8.1",
"@monaco-editor/react": "4.4.5", "@monaco-editor/react": "4.4.5",
"@nrwl/cli": "^15.7.1", "@nrwl/cli": "15.7.1",
"@nrwl/eslint-plugin-nx": "^15.7.1", "@nrwl/eslint-plugin-nx": "15.7.1",
"@nrwl/js": "15.7.1", "@nrwl/js": "15.7.1",
"@nrwl/linter": "15.7.1", "@nrwl/linter": "15.7.1",
"@nrwl/node": "15.7.1", "@nrwl/node": "15.7.1",
"@nrwl/react": "15.7.1", "@nrwl/react": "15.7.1",
"@nrwl/tao": "^15.7.1", "@nrwl/tao": "15.7.1",
"@nrwl/web": "15.7.1", "@nrwl/web": "15.7.1",
"@nrwl/webpack": "15.7.1", "@nrwl/webpack": "15.7.1",
"@nrwl/workspace": "^15.7.1", "@nrwl/workspace": "^15.7.1",
@ -264,6 +273,7 @@
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^5.40.1",
"@uniswap/v2-core": "^1.0.1", "@uniswap/v2-core": "^1.0.1",
"@uniswap/v3-core": "^1.0.1", "@uniswap/v3-core": "^1.0.1",
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
"ace-mode-lexon": "^1.*.*", "ace-mode-lexon": "^1.*.*",
"ace-mode-move": "0.0.1", "ace-mode-move": "0.0.1",
"ace-mode-solidity": "^0.1.0", "ace-mode-solidity": "^0.1.0",
@ -295,6 +305,7 @@
"css-minimizer-webpack-plugin": "^4.2.2", "css-minimizer-webpack-plugin": "^4.2.2",
"csslint": "^1.0.2", "csslint": "^1.0.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"electron": "^24.4.0",
"eslint": "^8.26.0", "eslint": "^8.26.0",
"eslint-config-standard": "^14.1.1", "eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",

@ -10,9 +10,9 @@
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"importHelpers": true, "importHelpers": true,
"target": "ES2015", "target": "ES6",
"esModuleInterop": true, "esModuleInterop": true,
"module": "CommonJS", "module": "ES6",
"lib": [ "lib": [
"es2017", "es2017",
"dom", "dom",

@ -154,6 +154,10 @@
"@remixproject/walletconnect-plugin": [ "@remixproject/walletconnect-plugin": [
"apps/walletconnect/src/index.ts" "apps/walletconnect/src/index.ts"
], ],
"@remix-ui/xterm": [
"libs/remix-ui/xterm/src/index.ts"
],
} }
} }
} }

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save