Code Monkey home page Code Monkey logo

omelette's Introduction

Omelette is a simple template based autocompletion tool for Node and Deno projects with super easy API.

npm version Build Status

yarn add omelette
# or
npm install omelette

You also can use Omelette with Deno:

import omelette from "https://deno.land/x/omelette/omelette.ts";

You just have to decide your program name and CLI fragments.

omelette`github ${['pull', 'push']} ${['origin', 'upstream']} ${['master', 'develop']}`.init()

...and you are almost done! The output will look like this:

Quick Start

For a step by step guide please follow this link

Implementing omelette is very easy:

import * as omelette from 'omelette';

const firstArgument = ({ reply }) => {
  reply([ 'beautiful', 'cruel', 'far' ])
}

const planet = ({ reply }) => {
  reply([ 'world', 'mars', 'pluto' ])
}

omelette`hello|hi ${firstArgument} ${planet}`.init()

Simple Event Based API ☕️

It's based on a simple CLI template.

Let's think we have a executable file with the name githubber, in a global path.

In our program, the code will be:

import * as omelette from 'omelette';

// Write your CLI template.
const completion = omelette(`githubber|gh <action> <user> <repo>`);

// Bind events for every template part.
completion.on('action', ({ reply }) => {
  reply([ 'clone', 'update', 'push' ])
})

completion.on('user', ({ reply }) => {
  reply(fs.readdirSync('/Users/'))
})

completion.on('repo', ({ before, reply }) => {
  reply([
    `http://github.com/${before}/helloworld`,
    `http://github.com/${before}/blabla`
  ])
})

// Initialize the omelette.
completion.init()

// If you want to have a setup feature, you can use `omeletteInstance.setupShellInitFile()` function.
if (~process.argv.indexOf('--setup')) {
  completion.setupShellInitFile()
}

// Similarly, if you want to tear down autocompletion, use `omeletteInstance.cleanupShellInitFile()`
if (~process.argv.indexOf('--cleanup')) {
  completion.cleanupShellInitFile()
}

// Rest is yours
console.log("Your program's default workflow.")
console.log(process.argv)

complete.reply is the completion replier. You must pass the options into that method.

ES6 Template Literal API 🚀

You can use Template Literals to define your completion with a simpler (super easy) API.

import * as omelette from 'omelette';

// Just pass a template literal to use super easy API.
omelette`hello ${[ 'cruel', 'nice' ]} ${[ 'world', 'mars' ]}`.init()

Let's make the example above with ES6 TL:

import * as omelette from 'omelette'

// Write your CLI template.
omelette`
  githubber|gh

  ${[ 'clone', 'update', 'push' ]}
  ${() => fs.readdirSync('/Users/')}
  ${({ before }) => [
    `http://github.com/${before}/helloworld`,
    `http://github.com/${before}/blabla`,
  ]}
`.init()

Also you can still use lambda functions to make more complex template literals:

Advanced Template Literals

import * as omelette from 'omelette';

omelette`
  githubber|gh
      ${['pull', 'push', 'star'] /* Direct command list */}
      ${require('some/other/commands') /* Import from another file */}
      ${getFromRemote('http://api.example.com/commands') /* Remote call at the beginning */}
      ${({ reply }) => fetch('http://api.example.com/lazy-commands').then(reply) /* Fetch when argument <tab>bed */}
      ${() => fs.readdirSync("/Users/") /* Access filesystem via Node */}
      ${({ before }) => [ /* Use parameters like `before`, `line`, `fragment` or `reply` */
        `${before}/helloworld`,
        `${before}/blabla`
      ]}
  `.init()

// No extra configuration required.

console.log("Your program's default workflow.")
console.log(process.argv)

Async API ⏩

Omelette allows you to use async functions. You have to use onAsync and to pass Promise object to the reply function.

complete.onAsync('user', async ({ reply }) => {
  reply(new Promise((resolve) => {
    fs.readdir('/Users/', (err, users) => {
      resolve(users)
    })
  }))
})

⚠️ A note about async handlers

If you are using async handlers, you have to use complete.next method to continue running your main workflow.

// ...

complete.onAsync('user', async ({ reply }) => {
  reply(new Promise((resolve) => {
    fs.readdir('/Users/', (err, users) => {
      resolve(users)
    })
  }))
})

// Instead of running directly, you need to set an handler to run your main workflow.
complete.next(()=> {
  console.log("Your program's default workflow.")
  console.log(process.argv)
})

// .init must be called after defining .next
complete.init()
// ...

Using util.promisify will make your async handlers easier.

import promisify from 'util';

complete.onAsync('user', async ({ reply }) => {
  reply(await promisify(fs.readdir)('/Users'))
})

Tree API 🌲

You can use simple objects as autocompletion definitions:

omelette('hello').tree({
  cruel: ['world', 'moon'],
  beautiful: ['mars', 'pluto']
}).init();

Install

Automated Install

⚠️ Not available for Deno runtime. You can make your users to put yourprogram --completion | source or yourprogram --completion-fish | source args explicitly to their shell config file.

Installing and making your users install the autocompletion feature is very simple.

You can use simply use setupShellInitFile function.

try {
  // Pick shell init file automatically
  complete.setupShellInitFile()

  // Or use a manually defined init file
  complete.setupShellInitFile('~/.my_bash_profile')

} catch (err) {
  // setupShellInitFile() throws if the used shell is not supported
}

If you use Bash, it will create a file at ~/.<program-name>/completion.sh and append a loader code to ~/.bash_profile file.

If you use Zsh, it appends a loader code to ~/.zshrc file.

If you use Fish, it appends a loader code to ~/.config/fish/config.fish file.

TL;DR: It does the Manual Install part, basically.

Automated Uninstallation

⚠️ Not available for Deno runtime. Your users need to remove the autocompletion setup script from their shell config files.

Similarly to installation, you can use cleanupShellInitFile to undo changes done by setupShellInitFile.

complete.cleanupShellInitFile()

As with setupShellInitFile(), wrap this in a try/catch block to handle unsupported shells.

Manual Installation

Instructions for your README files:

(You should add these instructions to your project's README, don't forget to replace myprogram string with your own executable name)

In zsh, you should write these:

echo '. <(myprogram --completion)' >> ~/.zshrc

In bash:

On macOS, you may need to install bash-completion using brew install bash-completion.

myprogram --completion >> ~/.config/hello.completion.sh
echo 'source ~/.config/hello.completion.sh' >> ~/.bash_profile

In fish:

echo 'myprogram --completion-fish | source' >> ~/.config/fish/config.fish

That's all!

Now you have an autocompletion system for your CLI tool.

Additions

There are some useful additions to omelette.

Parameters

Callbacks have two parameters:

  • The fragment name (e.g.command of <command> template) (only in global event)
  • The meta data
    • fragment: The number of fragment.
    • before: The previous word.
    • line: The whole command line buffer allow you to parse and reply as you wish.
    • reply: This is the reply function to use this-less API.

Global Event

You can also listen to all fragments by "complete" event.

complete.on('complete', (fragment, { reply }) => reply(["hello", "world"]));

Numbered Arguments

You can also listen to events in order.

complete.on('$1', ({ reply }) => reply(["hello", "world"]))

Autocompletion Tree

You can create a completion tree to more complex autocompletions.

omelette('hello').tree({
  how: {
    much: {
      is: {
        this: ['car'],
        that: ['house'],
      }
    },
    are: ['you'],
    many: ['cars', 'houses'],
  },
  where: {
    are: {
      you: ['from'],
      the: ['houses', 'cars'],
    },
    is: {
      // You can also add some logic with defining functions:
      your() {
        return ['house', 'car'];
      },
    }
  },
}).init()

Now, you will be able to use your completion as tree.

Thanks @jblandry for the idea.

Advanced Tree Implementations

You can seperate your autocompletion by importing objects from another file:

omelette('hello').tree(require('./autocompletion-tree.js')).init();

Short Names

You can set a short name for an executable:

In this example, githubber is long and gh is short.

omelette('githubber|gh <module> <command> <suboption>');

Test

Now you can try it in your shell.

git clone https://github.com/f/omelette
cd omelette/example
alias githubber="./githubber" # The app should be global, completion will search it on global level.
./githubber --setup --debug # --setup is not provided by omelette, you should proxy it.
# (reload bash, or source ~/.bash_profile or ~/.config/fish/config.fish)
omelette-debug-githubber # See Debugging section
githubber<tab>
ghb<tab> # short alias
gh<tab> # short alias

Debugging

--debug option generates a function called omelette-debug-<programname>. (omelette-debug-githubber in this example).

When you run omelette-debug-<programname>, it will create aliases for your application. (githubber and gh in this example).

A long name:

$ githubber<tab>
clone update push

Or short name:

$ gh<tab>
clone update push

Then you can start easily.

$ ./githubber<tab>
clone update push
$ ./githubber cl<tab>
$ ./githubber clone<tab>
Guest fka
$ ./githubber clone fka<tab>
$ ./githubber clone fka http://github.com/fka/<tab>
http://github.com/fka/helloworld
http://github.com/fka/blabla

Using with Deno

Omelette now supports and is useful with Deno. You can make your Deno based CLI tools autocomplete powered using Omelette. It's fully featured but setupShellInitFile and cleanupShellInitFile methods does not exist for now (to prevent requirement of allow-env, allow-read and allow-write permissions).

Instructions to use Omelette in your Deno projects:

Assume we have a hello.js:

import omelette from "https://raw.githubusercontent.com/f/omelette/master/deno/omelette.ts";

const complete = omelette("hello <action>");

complete.on("action", function ({ reply }) {
  reply(["world", "mars", "jupiter"]);
});

complete.init();

// your CLI program

Install your program using deno install:

deno install hello.js
hello --completion | source # bash and zsh installation
hello --completion-fish | source # fish shell installation

That's all! Now you have autocompletion feature!

hello <tab><tab>

Users?

  • Office 365 CLI uses Omelette to support autocompletion in office365-cli.
  • Visual Studio App Center CLI uses Omelette to support autocompletion in appcenter-cli.

Contribute

I need your contributions to make that work better!

License

This project licensed under MIT.

omelette's People

Contributors

0xflotus avatar bencao avatar buzztaiki avatar canrau avatar danielrozenberg avatar devthejo avatar eliassjogreen avatar f avatar grant avatar jblandry avatar jolg42 avatar loilo avatar nkovshov avatar onlined avatar robertrosman avatar walkerrandolphsmith avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

omelette's Issues

Infinite Number of Flags

While educating myself on auto-complete, I was poking at npm and noticed that their autocomplete scripts allow for an infinite number of flags to be referenced by the completion mechanism, without regard to what position they're in. While the positioning that omelette provides is useful, removing that constraint would also be immensely useful. For reference, here is the script that results from running $ npm completion

###-begin-npm-completion-###
#
# npm command completion script
#
# Installation: npm completion >> ~/.bashrc  (or ~/.zshrc)
# Or, maybe: npm completion > /usr/local/etc/bash_completion.d/npm
#

if type complete &>/dev/null; then
  _npm_completion () {
    local words cword
    if type _get_comp_words_by_ref &>/dev/null; then
      _get_comp_words_by_ref -n = -n @ -n : -w words -i cword
    else
      cword="$COMP_CWORD"
      words=("${COMP_WORDS[@]}")
    fi

    local si="$IFS"
    IFS=$'\n' COMPREPLY=($(COMP_CWORD="$cword" \
                           COMP_LINE="$COMP_LINE" \
                           COMP_POINT="$COMP_POINT" \
                           npm completion -- "${words[@]}" \
                           2>/dev/null)) || return $?
    IFS="$si"
    if type __ltrim_colon_completions &>/dev/null; then
      __ltrim_colon_completions "${words[cword]}"
    fi
  }
  complete -o default -F _npm_completion npm
elif type compdef &>/dev/null; then
  _npm_completion() {
    local si=$IFS
    compadd -- $(COMP_CWORD=$((CURRENT-1)) \
                 COMP_LINE=$BUFFER \
                 COMP_POINT=0 \
                 npm completion -- "${words[@]}" \
                 2>/dev/null)
    IFS=$si
  }
  compdef _npm_completion npm
elif type compctl &>/dev/null; then
  _npm_completion () {
    local cword line point words si
    read -Ac words
    read -cn cword
    let cword-=1
    read -l line
    read -ln point
    si="$IFS"
    IFS=$'\n' reply=($(COMP_CWORD="$cword" \
                       COMP_LINE="$line" \
                       COMP_POINT="$point" \
                       npm completion -- "${words[@]}" \
                       2>/dev/null)) || return $?
    IFS="$si"
  }
  compctl -K _npm_completion npm
fi
###-end-npm-completion-###

Would it be possible to allow the same kind of "as many flags as you want in any position" functionality?

Publish v0.4.6 to npm

The latest release on GitHub is v0.4.6 but npm lists v0.4.5 as the latest version. Could you please publish v0.4.6 to npm?

Add files to package.json

Now npm package contains everythinng:

ll node_modules/omelette/

► ./node_modules/omelette

     96 B  coffee
    192 B  example
    224 B  resources
     96 B  src
     59 B  .travis.yml
      1kB  LICENSE
     11kB  README.md
     30 B  build.sh
      1kB  package.json

Please, add files section of restrict by .npmignore.

Usage with oclif

Love the work you've done here 🎉

Is it possible to integrate with a CLI bootstrapped with oclif?

promise.then(reply) not working

So I've thought I should be able to do something like the following

omelette`bin ${({reply}) => Promise.resolve(["a","b"]).then(reply)`

but for some reason this doesn't work, can you help me? Replying immediately works, returning the options array also works. But promises don't work in this way, even though it seems to be the exact same usage as in the readme.

Change of 'word' breaks 'before' param

Hi,
I tried to switch to 0.4.0 and the change of fetching method for word (b74b232#diff-564d24cceffda49c18bf427e92b8baa3R23) makes the parameter before undefined when an incomplete word is tabbed.

Ex:

lorem <tab><tab>      -> a1 b1       👍 
lorem a<tab>          -> a1          👍
lorem a1 <tab><tab>   -> a2 b2 c2    👍
lorem a1 a<tab>       -> nothing     👎 

My script that goes like this

const autocomplete = {
  'a1': {
    'a2': {
      'a3': {
        'a4': {},
        'b4': {}
      },
      'b3': {}
    },
    'b2': {},
    'c2': {}
  },
  'b1': {
    'a2': {},
    'b2': {
      'a3': {
        'a4': {},
        'b4': {}
      },
      'b3': {}
    },
    'c2': {}
  }
};


const completion = omelette('lorem');

completion.on('$1', ({ reply }) => {
  reply(Object.keys(autocomplete));
});

completion.on('$2', ({ reply, before }) => {
  try {
    reply(Object.keys(autocomplete[before]));
  } catch (e) {}
});

completion.on('$3', ({ reply, before, line }) => {
  const [, lvl1] = line.split(' ');
  try {
    reply(Object.keys(autocomplete[lvl1][before]));
  } catch (e) {}
});

completion.on('$4', ({ reply, before, line }) => {
  const [, lvl1, lvl2] = line.split(' ');
  try {
    reply(Object.keys(autocomplete[lvl1][lvl2][before]));
  } catch (e) {}
});

completion.init();

Enabling autocomplete without requiring step from user

So in the readme you have something like:

./githubber --completion >> ~/githubber.completion.sh
echo 'source ~/githubber.completion.sh' >> .bash_profile

And the user has to do this. Is there a way to automate this, because it's going to be hard to get the user to run something like that for every cli tool they want to use..

rfc: dynamic lookups

problem

i want to conditionally add more tabbables based on previous lookups

discussion

imagine:

const completion = omelette(`bestbin <root>`)

completion.on('root', ({ reply }) => {
  return reply([ 'a', 'b' ])
})

completion.on('b', ({ reply }) => {
  return reply(['c'])
})

completion.on('c', ({ reply }) => {
  return reply(['--d'])
})

completion.on('e', ({ reply }) => {
  return reply(['f'])
})

my args don't have a fixed length, so the bin <cmd> <structure> <of> <fixed> <arity> doesn't work for me. is this something you'd support?

Suggestion: Let .tree() accept callbacks

I love .tree() because it can represent nested dependencies:

omelette('hello').tree({
  cruel: ['world', 'moon'],
  beautiful: ['mars', 'pluto']
}).init();

However, it’d be great if it were possible to also dynamically create the top level via a callback:

omelette('hello').tree(
  async () => {
    const json = await /* ... */;
    return json;
  }
).init();

In my own code, I use the following workaround (TypeScript code):

const oml = omelette(BIN_NAME) as omelette.Instance & { compgen: number };
if (oml.compgen >= 0) {
  const tree: omelette.TreeValue = {};
  const projectDir = process.cwd();
  if (BookConfig.isProjectDir(projectDir)) {
    const bookConfig = BookConfig.fromDir(projectDir);
    for (const key of bookConfig.getOutputConfigKeys()) {
      tree[key] = ['--dry-run', '--replace'];
    }
  }
  oml.tree(tree).init();
  return; // not really needed
}

Allow completion entries to have descriptions

Some shells support more than simple string completions. Each entry can have a description.

Here is a screenshot from zsh. PowerShell also supports rich completion entries.
image

Is there a way to specify descriptions in omelette, so that supporting shells can have descriptions, and other shells are unaffected?

Node 8, npx integration ?

If I add autocompletion for my global cli, it works great (mycli <tab>), but if the same script is called through npx (which calls local binaries, npx mycli <tab>, available in Node 8) autocompletion won't work for the same script.

Wondering is there a way to solve it easily ?

Limit Package Contents to Code

Hey there. I work on a few projects where folks are overly sensitive about the install size of their packages. I noticed that omlette doesn't have a files property defined in package.json, and as a result, the entire directory contents including the coffee script are being added to the published package.

Any chance you'd be willing to publish with only the files the package needs?

leafs containing dashed prefixed values (-f, -b, --foo, --bar) is not working for tree autocompletion

When using the tree helper.
Leafs that containing dashed prefixed values seem to mess up the autocompletion.

does not work

const completion = omelette('foo')
const tree = {
    bar : ['--foobar', '-f']
}
completion.tree(tree).init()

works

const completion = omelette('foo')
completion.on('$2', ({reply}) => {
     reply(['--foobar', '-f'])
})
completion.init()

using:
bash 4.3-14ubuntu1.2
linux mint 18.2 (ubuntu 16.04)

How to use this module

I don't understand where the code from Get started should be placed and how do I get the auto completion functional in my terminal. It isn't clear enough how to get started using this module practically.

Thanks.

How do I go about troubleshooting autocompletion not working in Zsh?

I have a setup/cleanup script that successfully adds and removes:

# begin app completion
. <(app --completion)
# end app completion

I have also setup this alias in ~/.zshrc: alias lando='bin/app.js'.

app --completion outputs:

### app completion - begin. generated by omelette.js ###
if type compdef &>/dev/null; then
  _app_completion() {
    compadd -- `app --compzsh --compgen "${CURRENT}" "${words[CURRENT-1]}" "${BUFFER}"`
  }
  compdef _app_completion app
elif type complete &>/dev/null; then
  _app_completion() {
    local cur prev nb_colon
    _get_comp_words_by_ref -n : cur prev
    nb_colon=$(grep -o ":" <<< "$COMP_LINE" | wc -l)

    COMPREPLY=( $(compgen -W '$(app --compbash --compgen "$((COMP_CWORD - (nb_colon * 2)))" "$prev" "${COMP_LINE}")' -- "$cur") )

    __ltrim_colon_completions "$cur"
  }
  complete -F _app_completion app
fi
### app completion - end ###

My app.js contains:

    // Write your CLI template.
    var completion = omelette('app <test> <test2>')

// This is your logic
    completion.on('test', ({reply}) => { reply(['option1', 'option2']) });
    completion.on('test2', ({reply}) => { reply(['option3', 'option4']) });

    completion.init();

Now, I expect when that when I type app<tab> that I will see the options, but I do not. What are my next steps to troubleshooting? What command outputs the options that I hope to see? Is it possible that my JS is outputting garbage prematurely that breaks Zsh's autocompletion?

Automatic install makes zsh to hang forever

Hello,

Great tool! I have had a great pleasure to use it to create my own autocomplete stuff :-)

I have used the .setupShellInitFile() function to automatically setup the shell autocompletion. However when I do so and run source ~/.zshrc, it hangs forever. I have to kill -9 the hanging process.

The added line in the .zshrc files is the following:

# .zshrc
. <(app --completion)

When I turn it into the following one, it does not hang and autocompletion works:

eval $(app --completion)

I am unsure about this issue.
Do you have an idea?

Windows support

Great tool, Is this suppose to work on windows with CMD, powershell or git bash?

Could we add a license file?

Hello and thanks for publishing this awesome project!
I was considering using this for a project and wanted to make sure I'm doing the right thing - but saw that there's no license file. I'm not sure if the (currently) single line at the end of the readme file applicable.
Would you accept a pull request with the MIT license?
Otherwise (from https://help.github.com/articles/adding-a-license-to-a-repository/), looks like you can manually add a license file to the repo.
Regards
Jonathan

Add uninstall script

It would be immensely useful to have a counterpart to setupShellInitFile(), something like cleanupShellInitFile().

That would allow to run setupShellInitFile() in an npm postinstall hook. Doing this right now will append another completion block to the shell init file each time npm i -g cli-package is run (e.g. to update the CLI).

cleanupShellInitFile() could be used in a postuninstall hook to make sure there will never be more than one completion block.

Integration with Yeoman?

Hello! I've created tool for automating directory/file duplication and renaming, etc. The program is based on Yeoman (http://yeoman.io/).

Instead of binding Omellette's autocomplete functionality to a certain command (program), can it be bound to a certain function within a program?

For example, I run my automation tool by running yo tcg in Terminal ... then the user is immediately confronted with a series of prompts. It would be rad if Omelette could be used within these prompts (for the autocompletion of directory names, etc).

Is something like possible?

Fall back to _filedir, compgen -f, etc. if no matches are returned

Programs that use omelette completion can no longer complete files and directories. I modified my completion script to fall back to _filedir (it relies on the $cur and $prev variables):

  _p_complette() {
    local cur prev

    cur=${COMP_WORDS[COMP_CWORD]}
    prev=${COMP_WORDS[COMP_CWORD-1]}

    COMPREPLY=( $(compgen -W '$(projects --compbash --compgen "${COMP_CWORD}" "${prev}" "${COMP_LINE}")' -- "$cur") )

    [[ $COMPREPLY ]] && return

    _filedir
  }
  complete -F _p_complette p

In the same way compgen -f with compopt -o filenames could be used, I think.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.